From 670944f7048b0fceef88eb893487c66fa41d7747 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 13 Jun 2024 10:33:00 +0200 Subject: [PATCH] fix: [ANDROAPP-6131] Add progress when event list is loading Signed-off-by: Pablo --- .../datasetList/mapper/DatasetCardMapper.kt | 3 + .../ProgramEventDetailActivity.kt | 4 - .../ProgramEventDetailLiveAdapter.kt | 125 ------------------ .../ProgramEventDetailPresenter.kt | 1 - .../ProgramEventDetailRepository.kt | 8 +- .../ProgramEventDetailRepositoryImpl.kt | 24 +--- .../ProgramEventDetailView.kt | 1 - .../ProgramEventDetailViewModel.kt | 2 +- .../eventList/EventListFragment.kt | 116 ++++------------ .../eventList/EventListFragmentView.kt | 8 +- .../eventList/EventListInjector.kt | 26 ++-- .../eventList/EventListPresenter.kt | 38 ------ .../eventList/EventListPresenterFactory.kt | 27 ++++ .../eventList/EventListScreen.kt | 104 +++++++++++++++ .../eventList/EventListViewModel.kt | 85 ++++++++++++ .../eventList/ui/mapper/EventCardMapper.kt | 76 ++++++----- .../ui/mapper/TEICardMapper.kt | 1 + .../teievents/ui/mapper/TEIEventCardMapper.kt | 19 +-- .../fragment_program_event_detail_list.xml | 44 ------ commons/build.gradle.kts | 1 + .../dhis2/commons/filters/FilterManager.java | 22 ++- .../filters/FilterManagerExtensions.kt | 14 ++ .../dhis2/commons/ui/model/ListCardUiModel.kt | 1 + gradle/libs.versions.toml | 4 + 24 files changed, 363 insertions(+), 391 deletions(-) delete mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt delete mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenter.kt create mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenterFactory.kt create mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListScreen.kt create mode 100644 app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListViewModel.kt delete mode 100644 app/src/main/res/layout/fragment_program_event_detail_list.xml create mode 100644 commons/src/main/java/org/dhis2/commons/filters/FilterManagerExtensions.kt diff --git a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/mapper/DatasetCardMapper.kt b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/mapper/DatasetCardMapper.kt index 0a21fb02e6..b08eb5750f 100644 --- a/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/mapper/DatasetCardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/datasets/datasetDetail/datasetList/mapper/DatasetCardMapper.kt @@ -36,6 +36,9 @@ class DatasetCardMapper( onCardCLick: () -> Unit, ): ListCardUiModel { return ListCardUiModel( + id = with(dataset) { + "${datasetUid()}_${periodId()}_${orgUnitUid()}_${catOptionComboUid()}" + }, title = dataset.namePeriod(), lastUpdated = dataset.lastUpdated().toDateSpan(context), additionalInfo = getAdditionalInfoList(dataset, editable), diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt index fed6c83568..3d6aa20f87 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailActivity.kt @@ -248,10 +248,6 @@ class ProgramEventDetailActivity : binding.name = programModel.displayName() } - override fun showFilterProgress() { - programEventsViewModel.setProgress(true) - } - override fun renderError(message: String) { if (activity != null) { MaterialAlertDialogBuilder(activity, R.style.MaterialDialog) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt deleted file mode 100644 index 28304f5177..0000000000 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailLiveAdapter.kt +++ /dev/null @@ -1,125 +0,0 @@ -package org.dhis2.usescases.programEventDetail - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.testTag -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.paging.PagedListAdapter -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.recyclerview.widget.DiffUtil -import org.dhis2.R -import org.dhis2.commons.data.EventViewModel -import org.dhis2.commons.resources.ColorUtils -import org.dhis2.databinding.ItemEventBinding -import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper -import org.dhis2.usescases.teiDashboard.dashboardfragments.teidata.teievents.EventViewHolder -import org.hisp.dhis.android.core.program.Program -import org.hisp.dhis.mobile.ui.designsystem.component.ListCard -import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel -import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing - -class ProgramEventDetailLiveAdapter( - private val program: Program, - private val eventViewModel: ProgramEventDetailViewModel, - private val colorUtils: ColorUtils, - private val cardMapper: EventCardMapper, - config: AsyncDifferConfig, -) : PagedListAdapter(config) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder { - val inflater = LayoutInflater.from(parent.context) - val binding = ItemEventBinding.inflate(inflater, parent, false) - return EventViewHolder( - binding, - program, - colorUtils, - { eventUid -> - eventViewModel.eventSyncClicked.value = eventUid - }, - { _, _ -> }, - { eventUid, orgUnitUid, _, _ -> - eventViewModel.eventClicked.value = Pair(eventUid, orgUnitUid) - }, - ) - } - - override fun onBindViewHolder(holder: EventViewHolder, position: Int) { - getItem(position)?.let { - val materialView = holder.itemView.findViewById(R.id.materialView) - materialView.visibility = View.GONE - val composeView = holder.itemView.findViewById(R.id.composeView) - composeView.setContent { - val card = cardMapper.map( - event = it, - editable = it.event?.uid() - ?.let { eventViewModel.isEditable(it) } ?: true, - displayOrgUnit = it.event?.program() - ?.let { program -> eventViewModel.displayOrganisationUnit(program) } - ?: true, - onSyncIconClick = { - eventViewModel.eventSyncClicked.value = it.event?.uid() - }, - onCardClick = { - it.event?.let { event -> - eventViewModel.eventClicked.value = - Pair(event.uid(), event.organisationUnit() ?: "") - } - }, - ) - Column( - modifier = Modifier - .padding( - start = Spacing.Spacing8, - end = Spacing.Spacing8, - bottom = Spacing.Spacing4, - ), - ) { - if (position == 0) { - Spacer(modifier = Modifier.size(Spacing.Spacing8)) - } - ListCard( - modifier = Modifier.testTag("EVENT_ITEM"), - listAvatar = card.avatar, - title = ListCardTitleModel(text = card.title), - lastUpdated = card.lastUpdated, - additionalInfoList = card.additionalInfo, - actionButton = card.actionButton, - expandLabelText = card.expandLabelText, - shrinkLabelText = card.shrinkLabelText, - onCardClick = card.onCardCLick, - ) - } - } - - holder.bind(it, null) { - getItem(holder.bindingAdapterPosition)?.toggleValueList() - notifyItemChanged(holder.bindingAdapterPosition) - } - } - } - - companion object { - val diffCallback: DiffUtil.ItemCallback - get() = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: EventViewModel, - newItem: EventViewModel, - ): Boolean { - return oldItem.event?.uid() == newItem.event?.uid() - } - - override fun areContentsTheSame( - oldItem: EventViewModel, - newItem: EventViewModel, - ): Boolean { - return oldItem == newItem - } - } - } -} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt index 33ba7f44f5..d75b26e04d 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailPresenter.kt @@ -94,7 +94,6 @@ class ProgramEventDetailPresenter( ) compositeDisposable.add( filterManager.asFlowable().onBackpressureLatest() - .doOnNext { view.showFilterProgress() } .subscribeOn(schedulerProvider.io()) .observeOn(schedulerProvider.ui()) .subscribe( diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt index dc9d805a4b..b3d127cae6 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepository.kt @@ -1,19 +1,19 @@ package org.dhis2.usescases.programEventDetail -import androidx.lifecycle.LiveData -import androidx.paging.PagedList +import androidx.paging.PagingData import io.reactivex.Flowable import io.reactivex.Single -import org.dhis2.commons.data.EventViewModel +import kotlinx.coroutines.flow.Flow import org.dhis2.commons.data.ProgramEventViewModel import org.hisp.dhis.android.core.category.CategoryOptionCombo import org.hisp.dhis.android.core.common.FeatureType +import org.hisp.dhis.android.core.event.Event import org.hisp.dhis.android.core.event.EventFilter import org.hisp.dhis.android.core.program.Program import org.hisp.dhis.android.core.program.ProgramStage interface ProgramEventDetailRepository { - fun filteredProgramEvents(): LiveData> + fun filteredProgramEvents(): Flow> fun filteredEventsForMap(): Flowable fun program(): Single fun getAccessDataWrite(): Boolean diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt index 1d73fec3a3..7834dfca3b 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailRepositoryImpl.kt @@ -1,14 +1,11 @@ package org.dhis2.usescases.programEventDetail -import androidx.lifecycle.LiveData -import androidx.paging.DataSource -import androidx.paging.LivePagedListBuilder -import androidx.paging.PagedList +import androidx.paging.PagingData import com.mapbox.geojson.FeatureCollection import dhis2.org.analytics.charts.Charts import io.reactivex.Flowable import io.reactivex.Single -import org.dhis2.commons.data.EventViewModel +import kotlinx.coroutines.flow.Flow import org.dhis2.commons.data.ProgramEventViewModel import org.dhis2.commons.filters.data.FilterPresenter import org.dhis2.maps.geometry.mapper.featurecollection.MapCoordinateFieldToFeatureCollection @@ -41,22 +38,11 @@ class ProgramEventDetailRepositoryImpl internal constructor( filterPresenter.filteredEventProgram(it) } - override fun filteredProgramEvents(): LiveData> { + override fun filteredProgramEvents(): Flow> { val program = program().blockingGet() ?: throw NullPointerException() - val dataSource = filterPresenter + return filterPresenter .filteredEventProgram(program) - .dataSource - .map { event -> - mapper.eventToEventViewModel(event) - } - return LivePagedListBuilder( - object : DataSource.Factory() { - override fun create(): DataSource { - return dataSource - } - }, - 20, - ).build() + .getPagingData(10) } override fun filteredEventsForMap(): Flowable { diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailView.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailView.kt index 37a03aebda..736339e7af 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailView.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailView.kt @@ -10,7 +10,6 @@ interface ProgramEventDetailView : AbstractActivityContracts.View { fun renderError(message: String) fun showHideFilter() fun setWritePermission(canWrite: Boolean) - fun showFilterProgress() fun updateFilters(totalFilters: Int) fun openOrgUnitTreeSelector() fun showPeriodRequest(periodRequest: PeriodRequest) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt index 68e21c5fc9..b01a20e640 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/ProgramEventDetailViewModel.kt @@ -19,7 +19,7 @@ class ProgramEventDetailViewModel( val dispatcher: DispatcherProvider, val createEventUseCase: CreateEventUseCase, ) : ViewModel() { - private val progress = MutableLiveData(true) + private val progress = MutableLiveData(false) val writePermission = MutableLiveData(false) val eventSyncClicked = MutableLiveData(null) val eventClicked = MutableLiveData?>(null) diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt index 34374c972d..69de9cf496 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragment.kt @@ -4,126 +4,64 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.layout.padding -import androidx.compose.ui.Modifier +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import androidx.recyclerview.widget.AsyncDifferConfig -import androidx.test.espresso.idling.concurrent.IdlingThreadPoolExecutor -import org.dhis2.R -import org.dhis2.commons.data.EventViewModel -import org.dhis2.commons.filters.workingLists.WorkingListChipGroup import org.dhis2.commons.filters.workingLists.WorkingListViewModel import org.dhis2.commons.filters.workingLists.WorkingListViewModelFactory -import org.dhis2.commons.resources.ColorUtils -import org.dhis2.databinding.FragmentProgramEventDetailListBinding import org.dhis2.usescases.general.FragmentGlobalAbstract import org.dhis2.usescases.programEventDetail.ProgramEventDetailActivity -import org.dhis2.usescases.programEventDetail.ProgramEventDetailLiveAdapter import org.dhis2.usescases.programEventDetail.ProgramEventDetailViewModel import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper -import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing -import java.util.concurrent.Executors -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit import javax.inject.Inject -class EventListFragment : FragmentGlobalAbstract(), EventListFragmentView { - - lateinit var binding: FragmentProgramEventDetailListBinding - private var liveAdapter: ProgramEventDetailLiveAdapter? = null - private val programEventsViewModel: ProgramEventDetailViewModel by activityViewModels() - private var liveDataList: LiveData>? = null +class EventListFragment : FragmentGlobalAbstract() { @Inject - lateinit var presenter: EventListPresenter + lateinit var eventListViewModelFactory: EventListPresenterFactory @Inject - lateinit var colorUtils: ColorUtils + lateinit var workingListViewModelFactory: WorkingListViewModelFactory @Inject lateinit var cardMapper: EventCardMapper - @Inject - lateinit var workingListViewModelFactory: WorkingListViewModelFactory - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { (activity as ProgramEventDetailActivity).component - ?.plus(EventListModule(this)) + ?.plus(EventListModule()) ?.inject(this) - programEventsViewModel.setProgress(true) - val bgThreadPoolExecutor = IdlingThreadPoolExecutor( - "DiffExecutor", - 2, - 2, - 0L, - TimeUnit.MILLISECONDS, - LinkedBlockingQueue(), - Executors.defaultThreadFactory(), - ) - - val config = AsyncDifferConfig.Builder(ProgramEventDetailLiveAdapter.diffCallback) - .setBackgroundThreadExecutor(bgThreadPoolExecutor) - .build() - - val program = presenter.program() ?: throw NullPointerException() - liveAdapter = - ProgramEventDetailLiveAdapter( - program, - programEventsViewModel, - colorUtils, - cardMapper, - config, - ) - return FragmentProgramEventDetailListBinding.inflate(inflater, container, false) - .apply { - binding = this - recycler.adapter = liveAdapter - configureWorkingList() - }.root - } + return ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + val workingListViewModel by viewModels { workingListViewModelFactory } + val eventListViewModel by viewModels { eventListViewModelFactory } + val programEventsViewModel by activityViewModels() + val cardClicked by eventListViewModel.onEventCardClick.collectAsState(null) + val syncClicked by eventListViewModel.onSyncClick.collectAsState(null) - override fun onResume() { - super.onResume() - programEventsViewModel.setProgress(true) - presenter.init() - } + LaunchedEffect(key1 = cardClicked) { + cardClicked?.let { + programEventsViewModel.eventClicked.value = it + } + } - override fun setLiveData(pagedListLiveData: LiveData>) { - liveDataList?.removeObservers(viewLifecycleOwner) - this.liveDataList = pagedListLiveData - liveDataList?.observe(viewLifecycleOwner) { pagedList: PagedList -> - programEventsViewModel.setProgress(false) - liveAdapter?.submitList(pagedList) { - if ((binding.recycler.adapter?.itemCount ?: 0) == 0) { - binding.emptyTeis.text = getString(R.string.empty_tei_add) - binding.emptyTeis.visibility = View.VISIBLE - binding.recycler.visibility = View.GONE - } else { - binding.emptyTeis.visibility = View.GONE - binding.recycler.visibility = View.VISIBLE + LaunchedEffect(key1 = syncClicked) { + programEventsViewModel.eventSyncClicked.value = syncClicked } - EventListIdlingResourceSingleton.decrement() - } - } - } - private fun configureWorkingList() { - binding.filterLayout.apply { - setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, - ) - setContent { - val workingListViewModel by viewModels { workingListViewModelFactory } - WorkingListChipGroup(Modifier.padding(top = Spacing.Spacing16), workingListViewModel) + EventListScreen( + eventListViewModel, + workingListViewModel, + ) } } } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragmentView.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragmentView.kt index 8ef6e8f995..8a7ceed0dd 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragmentView.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListFragmentView.kt @@ -1,9 +1,3 @@ package org.dhis2.usescases.programEventDetail.eventList -import androidx.lifecycle.LiveData -import androidx.paging.PagedList -import org.dhis2.commons.data.EventViewModel - -interface EventListFragmentView { - fun setLiveData(pagedListLiveData: LiveData>) -} +interface EventListFragmentView diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListInjector.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListInjector.kt index 538e12dd2e..de1b9c1bf4 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListInjector.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListInjector.kt @@ -5,9 +5,10 @@ import dagger.Provides import dagger.Subcomponent import org.dhis2.commons.di.dagger.PerFragment import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.prefs.PreferenceProvider -import org.dhis2.commons.schedulers.SchedulerProvider +import org.dhis2.commons.viewmodel.DispatcherProvider import org.dhis2.usescases.programEventDetail.ProgramEventDetailRepository +import org.dhis2.usescases.programEventDetail.ProgramEventMapper +import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper @PerFragment @Subcomponent(modules = [EventListModule::class]) @@ -16,23 +17,22 @@ interface EventListComponent { } @Module -class EventListModule( - val view: EventListFragmentView, -) { +class EventListModule { @Provides @PerFragment - fun providePresenter( + fun providePresenterFactory( filterManager: FilterManager, programEventDetailRepository: ProgramEventDetailRepository, - preferences: PreferenceProvider, - schedulers: SchedulerProvider, - ): EventListPresenter { - return EventListPresenter( - view, + dispatcher: DispatcherProvider, + mapper: ProgramEventMapper, + cardMapper: EventCardMapper, + ): EventListPresenterFactory { + return EventListPresenterFactory( filterManager, programEventDetailRepository, - preferences, - schedulers, + dispatcher, + mapper, + cardMapper, ) } } diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenter.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenter.kt deleted file mode 100644 index e2439f9514..0000000000 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.dhis2.usescases.programEventDetail.eventList - -import io.reactivex.disposables.CompositeDisposable -import org.dhis2.commons.filters.FilterManager -import org.dhis2.commons.prefs.PreferenceProvider -import org.dhis2.commons.schedulers.SchedulerProvider -import org.dhis2.commons.schedulers.defaultSubscribe -import org.dhis2.usescases.programEventDetail.ProgramEventDetailRepository -import org.hisp.dhis.android.core.program.Program -import timber.log.Timber - -class EventListPresenter( - val view: EventListFragmentView, - val filterManager: FilterManager, - val eventRepository: ProgramEventDetailRepository, - val preferences: PreferenceProvider, - val schedulerProvider: SchedulerProvider, -) { - - val disposable = CompositeDisposable() - - fun init() { - disposable.add( - filterManager.asFlowable().startWith(filterManager) - .doOnEach { EventListIdlingResourceSingleton.increment() } - .map { eventRepository.filteredProgramEvents() } - .defaultSubscribe( - schedulerProvider, - { view.setLiveData(it) }, - { Timber.e(it) }, - ), - ) - } - - fun program(): Program? { - return eventRepository.program().blockingGet() - } -} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenterFactory.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenterFactory.kt new file mode 100644 index 0000000000..3e342c0c31 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListPresenterFactory.kt @@ -0,0 +1,27 @@ +package org.dhis2.usescases.programEventDetail.eventList + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.usescases.programEventDetail.ProgramEventDetailRepository +import org.dhis2.usescases.programEventDetail.ProgramEventMapper +import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper + +class EventListPresenterFactory( + private val filterManager: FilterManager, + private val programEventDetailRepository: ProgramEventDetailRepository, + private val dispatchers: DispatcherProvider, + private val mapper: ProgramEventMapper, + private val cardMapper: EventCardMapper, +) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return EventListViewModel( + filterManager, + programEventDetailRepository, + dispatchers, + mapper, + cardMapper, + ) as T + } +} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListScreen.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListScreen.kt new file mode 100644 index 0000000000..6a6a5d54aa --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListScreen.kt @@ -0,0 +1,104 @@ +package org.dhis2.usescases.programEventDetail.eventList + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.paging.LoadState +import androidx.paging.compose.collectAsLazyPagingItems +import org.dhis2.R +import org.dhis2.commons.filters.workingLists.WorkingListChipGroup +import org.dhis2.commons.filters.workingLists.WorkingListViewModel +import org.hisp.dhis.mobile.ui.designsystem.component.ListCard +import org.hisp.dhis.mobile.ui.designsystem.component.ListCardTitleModel +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicator +import org.hisp.dhis.mobile.ui.designsystem.component.ProgressIndicatorType +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing + +@Composable +fun EventListScreen( + eventListViewModel: EventListViewModel, + workingListViewModel: WorkingListViewModel, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 8.dp), + verticalArrangement = Arrangement.Absolute.spacedBy(Spacing.Spacing8), + ) { + WorkingListChipGroup( + Modifier.padding(top = Spacing.Spacing16), + workingListViewModel, + ) + val events = eventListViewModel.eventList.collectAsLazyPagingItems() + when (events.loadState.refresh) { + is LoadState.Error -> {} + LoadState.Loading -> { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.Center, + ) { + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR) + } + } + + is LoadState.NotLoading -> { + if (events.itemCount < 1) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 42.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(id = R.string.empty_tei_add), + ) + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalArrangement = Arrangement.Absolute.spacedBy(8.dp), + ) { + items(count = events.itemCount) { index -> + val card = events[index]!! + ListCard( + modifier = Modifier.testTag("EVENT_ITEM"), + listAvatar = card.avatar, + title = ListCardTitleModel(text = card.title), + lastUpdated = card.lastUpdated, + additionalInfoList = card.additionalInfo, + actionButton = card.actionButton, + expandLabelText = card.expandLabelText, + shrinkLabelText = card.shrinkLabelText, + onCardClick = card.onCardCLick, + ) + + if (index == events.itemCount - 1) { + Spacer(modifier = Modifier.padding(100.dp)) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListViewModel.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListViewModel.kt new file mode 100644 index 0000000000..47aafcd3d9 --- /dev/null +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/EventListViewModel.kt @@ -0,0 +1,85 @@ +package org.dhis2.usescases.programEventDetail.eventList + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.map +import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.withContext +import org.dhis2.commons.filters.FilterManager +import org.dhis2.commons.ui.model.ListCardUiModel +import org.dhis2.commons.viewmodel.DispatcherProvider +import org.dhis2.usescases.programEventDetail.ProgramEventDetailRepository +import org.dhis2.usescases.programEventDetail.ProgramEventMapper +import org.dhis2.usescases.programEventDetail.eventList.ui.mapper.EventCardMapper + +class EventListViewModel( + val filterManager: FilterManager, + val eventRepository: ProgramEventDetailRepository, + val dispatchers: DispatcherProvider, + val mapper: ProgramEventMapper, + val cardMapper: EventCardMapper, +) : ViewModel() { + + val disposable = CompositeDisposable() + private var _onSyncClick: MutableStateFlow = MutableStateFlow(null) + val onSyncClick: Flow = _onSyncClick + private var _onEventCardClick: MutableStateFlow?> = MutableStateFlow(null) + val onEventCardClick: Flow?> = _onEventCardClick + + private var _eventList: Flow> = + filterManager.asFlow(viewModelScope) + .flatMapLatest { + eventRepository.filteredProgramEvents() + .map { pagingData -> + pagingData.map { event -> + withContext(dispatchers.io()) { + val eventModel = mapper.eventToEventViewModel(event) + cardMapper.map( + event = eventModel, + editable = eventRepository.isEventEditable(event.uid()), + displayOrgUnit = event.program()?.let { program -> + eventRepository.displayOrganisationUnit(program) + } ?: true, + onSyncIconClick = { + onSyncIconClick( + eventModel.event?.uid(), + ) + }, + onCardClick = { + eventModel.event?.let { event -> + onEventCardClick( + Pair( + event.uid(), + event.organisationUnit() ?: "", + ), + ) + } + }, + ) + } + } + }.flowOn(dispatchers.io()) + }.flowOn(dispatchers.io()) + + val eventList = _eventList + + private fun onSyncIconClick(eventUid: String?) { + viewModelScope.launch { + _onSyncClick.emit(eventUid) + } + } + + private fun onEventCardClick(eventUidAndOrgUnit: Pair) { + viewModelScope.launch { + _onEventCardClick.emit(eventUidAndOrgUnit) + } + } +} diff --git a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/ui/mapper/EventCardMapper.kt b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/ui/mapper/EventCardMapper.kt index 309a05642c..c4bd924226 100644 --- a/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/ui/mapper/EventCardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/programEventDetail/eventList/ui/mapper/EventCardMapper.kt @@ -43,11 +43,14 @@ class EventCardMapper( onCardClick: () -> Unit, ): ListCardUiModel { return ListCardUiModel( + id = event.event?.uid() ?: "", title = event.displayDate ?: "", lastUpdated = event.lastUpdate.toDateSpan(context), additionalInfo = getAdditionalInfoList(event, editable, displayOrgUnit), actionButton = { ProvideSyncButton( + syncButtonLabel = resourceManager.getString(R.string.sync), + retryButtonLabel = resourceManager.getString(R.string.sync_retry), state = event.event?.aggregatedSyncState(), onSyncIconClick = onSyncIconClick, ) @@ -231,40 +234,6 @@ class EventCardMapper( item?.let { list.add(it) } } - @Composable - private fun ProvideSyncButton(state: State?, onSyncIconClick: () -> Unit) { - val buttonText = when (state) { - State.TO_POST, - State.TO_UPDATE, - -> { - resourceManager.getString(R.string.sync) - } - - State.ERROR, - State.WARNING, - -> { - resourceManager.getString(R.string.sync_retry) - } - - else -> null - } - buttonText?.let { - Button( - style = ButtonStyle.TONAL, - text = it, - icon = { - Icon( - imageVector = Icons.Outlined.Sync, - contentDescription = it, - tint = TextColor.OnPrimaryContainer, - ) - }, - onClick = { onSyncIconClick() }, - modifier = Modifier.fillMaxWidth(), - ) - } - } - private fun checkSyncStatus( list: MutableList, state: State?, @@ -333,3 +302,42 @@ class EventCardMapper( item?.let { list.add(it) } } } + +@Composable +fun ProvideSyncButton( + syncButtonLabel: String, + retryButtonLabel: String, + state: State?, + onSyncIconClick: () -> Unit, +) { + val buttonText = when (state) { + State.TO_POST, + State.TO_UPDATE, + -> { + syncButtonLabel + } + + State.ERROR, + State.WARNING, + -> { + retryButtonLabel + } + + else -> null + } + buttonText?.let { + Button( + style = ButtonStyle.TONAL, + text = it, + icon = { + Icon( + imageVector = Icons.Outlined.Sync, + contentDescription = it, + tint = TextColor.OnPrimaryContainer, + ) + }, + onClick = { onSyncIconClick() }, + modifier = Modifier.fillMaxWidth(), + ) + } +} diff --git a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt index 2dd670c8c8..5e7cbfe62d 100644 --- a/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/searchTrackEntity/ui/mapper/TEICardMapper.kt @@ -50,6 +50,7 @@ class TEICardMapper( onImageClick: (String) -> Unit, ): ListCardUiModel { return ListCardUiModel( + id = searchTEIModel.tei.uid(), avatar = { ProvideAvatar(searchTEIModel, onImageClick) }, title = getTitle(searchTEIModel), lastUpdated = searchTEIModel.tei.lastUpdated().toDateSpan(context), diff --git a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt index e55f23b607..52e50f9091 100644 --- a/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt +++ b/app/src/main/java/org/dhis2/usescases/teiDashboard/dashboardfragments/teidata/teievents/ui/mapper/TEIEventCardMapper.kt @@ -43,6 +43,7 @@ class TEIEventCardMapper( onCardClick: () -> Unit, ): ListCardUiModel { return ListCardUiModel( + id = event.event?.uid() ?: "", avatar = if (event.groupedByStage != true) { { ProvideAvatar(eventItem = event) @@ -343,11 +344,12 @@ class TEIEventCardMapper( icon = { Icon( imageVector = Icons.Outlined.Edit, - contentDescription = resourceManager.getString(R.string.enter_event_data).format( - event.stage?.eventLabel() ?: resourceManager.getString( - R.string.event, + contentDescription = resourceManager.getString(R.string.enter_event_data) + .format( + event.stage?.eventLabel() ?: resourceManager.getString( + R.string.event, + ), ), - ), tint = TextColor.OnPrimaryContainer, ) }, @@ -365,11 +367,12 @@ class TEIEventCardMapper( icon = { Icon( imageVector = Icons.Outlined.Edit, - contentDescription = resourceManager.getString(R.string.enter_event_data).format( - event.stage?.eventLabel() ?: resourceManager.getString( - R.string.event, + contentDescription = resourceManager.getString(R.string.enter_event_data) + .format( + event.stage?.eventLabel() ?: resourceManager.getString( + R.string.event, + ), ), - ), tint = TextColor.OnPrimaryContainer, ) }, diff --git a/app/src/main/res/layout/fragment_program_event_detail_list.xml b/app/src/main/res/layout/fragment_program_event_detail_list.xml deleted file mode 100644 index f326932c95..0000000000 --- a/app/src/main/res/layout/fragment_program_event_detail_list.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 20ecdd06a0..ba60975d2a 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -92,6 +92,7 @@ dependencies { api(libs.androidx.compose.preview) api(libs.androidx.compose.ui) api(libs.androidx.compose.livedata) + api(libs.androidx.compose.paging) api(libs.google.material) api(libs.google.gson) diff --git a/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java b/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java index 61fc1f7cf7..337176f17e 100644 --- a/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java +++ b/commons/src/main/java/org/dhis2/commons/filters/FilterManager.java @@ -33,11 +33,18 @@ import io.reactivex.processors.PublishProcessor; import kotlin.Pair; import kotlin.collections.CollectionsKt; +import kotlinx.coroutines.CoroutineScope; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.MutableSharedFlow; +import kotlinx.coroutines.flow.MutableStateFlow; public class FilterManager implements Serializable { public void publishData() { filterProcessor.onNext(this); + if (scope != null) { + FilterManagerExtensionsKt.emit(this, scope, filterFlow); + } } public void setCatComboAdapter(CatOptCombFilterAdapter adapter) { @@ -91,6 +98,9 @@ public enum PeriodRequest { ); private FlowableProcessor filterProcessor; + private MutableSharedFlow filterFlow; + + private CoroutineScope scope; private FlowableProcessor ouTreeProcessor; private FlowableProcessor> periodRequestProcessor; private FlowableProcessor catOptComboRequestProcessor; @@ -137,7 +147,7 @@ public void reset() { eventStatusFilters = new ArrayList<>(); enrollmentStatusFilters = new ArrayList<>(); assignedFilter = false; - followUpFilter =false; + followUpFilter = false; sortingItem = null; ouFiltersApplied = new ObservableField<>(0); @@ -151,6 +161,7 @@ public void reset() { followUpFilterApplied = new ObservableField<>(0); filterProcessor = PublishProcessor.create(); + filterFlow = FilterManagerExtensionsKt.initFlow(this); ouTreeProcessor = PublishProcessor.create(); periodRequestProcessor = PublishProcessor.create(); catOptComboRequestProcessor = PublishProcessor.create(); @@ -258,7 +269,7 @@ public void addEnrollmentStatus(boolean remove, EnrollmentStatus enrollmentStatu boolean changed = true; if (remove) { enrollmentStatusFilters.remove(enrollmentStatus); - } else if (!enrollmentStatusFilters.contains(enrollmentStatus)){ + } else if (!enrollmentStatusFilters.contains(enrollmentStatus)) { enrollmentStatusFilters.clear(); enrollmentStatusFilters.add(enrollmentStatus); observableEnrollmentStatus.set(enrollmentStatus); @@ -296,7 +307,7 @@ public void addOrgUnit(OrganisationUnit ou) { publishData(); } - public void addOrgUnits(List ouList){ + public void addOrgUnits(List ouList) { ouFilters.clear(); ouFilters.addAll(ouList); liveDataOUFilter.setValue(ouFilters); @@ -351,6 +362,11 @@ public Flowable asFlowable() { return filterProcessor; } + public Flow asFlow(CoroutineScope scope) { + this.scope = scope; + return filterFlow; + } + public FlowableProcessor> getPeriodRequest() { return periodRequestProcessor; } diff --git a/commons/src/main/java/org/dhis2/commons/filters/FilterManagerExtensions.kt b/commons/src/main/java/org/dhis2/commons/filters/FilterManagerExtensions.kt new file mode 100644 index 0000000000..cfae6733a2 --- /dev/null +++ b/commons/src/main/java/org/dhis2/commons/filters/FilterManagerExtensions.kt @@ -0,0 +1,14 @@ +package org.dhis2.commons.filters + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlin.random.Random + +fun FilterManager.initFlow() = MutableStateFlow(0) +fun FilterManager.emit(scope: CoroutineScope, flow: MutableSharedFlow) { + scope.launch { + flow.emit(Random.nextInt()) + } +} diff --git a/commons/src/main/java/org/dhis2/commons/ui/model/ListCardUiModel.kt b/commons/src/main/java/org/dhis2/commons/ui/model/ListCardUiModel.kt index c926332d90..bc476386a8 100644 --- a/commons/src/main/java/org/dhis2/commons/ui/model/ListCardUiModel.kt +++ b/commons/src/main/java/org/dhis2/commons/ui/model/ListCardUiModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import org.hisp.dhis.mobile.ui.designsystem.component.AdditionalInfoItem data class ListCardUiModel( + val id: String, val avatar: (@Composable () -> Unit)? = null, val title: String, val description: String? = null, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f07b529e1..f54796b4a9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,8 @@ dynamicanimation = "1.0.0" viewpager2 = "1.0.0" recyclerview = "1.3.1" compose = "1.5.4" +composePaging = "3.3.0" +composeLifecycle ="2.8.1" composeTheme = "1.2.1" composeConstraintLayout = "1.0.1" activityCompose = "1.8.2" @@ -122,6 +124,8 @@ androidx-compose-livedata = { group = "androidx.compose.runtime", name = "runtim androidx-compose-uitooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" } androidx-compose-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" } androidx-compose-viewbinding = { group = "androidx.compose.ui", name = "ui-viewbinding", version.ref = "compose" } +androidx-compose-paging = {group = "androidx.paging", name="paging-compose", version.ref="composePaging"} +androidx-compose-lifecycle = {group= "androidx.lifecycle", name ="lifecycle-runtime-compose", version.ref="composeLifecycle"} androidx-coreKtx = { group = "androidx.core", name = "core-ktx", version.ref = "corektx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }