Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ dependencies {
def ktx2_version = "2.0.0"
def nav_version = "2.1.0-alpha05"
def anko_version = "0.10.8"
def paging_version = "2.1.0"

implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.multidex:multidex:2.0.1'
Expand Down Expand Up @@ -193,6 +194,11 @@ dependencies {
//LeakCanary
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2'

// Paging
implementation "androidx.paging:paging-runtime:$paging_version"
implementation "androidx.paging:paging-rxjava2:$paging_version"


testImplementation 'junit:junit:4.12'
testImplementation "io.mockk:mockk:1.9.3"
testImplementation 'org.threeten:threetenbp:1.4.0'
Expand Down
13 changes: 12 additions & 1 deletion app/src/main/java/org/fossasia/openevent/general/di/Modules.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.fossasia.openevent.general.di

import androidx.paging.PagedList
import androidx.room.Room
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.fasterxml.jackson.databind.DeserializationFeature
Expand Down Expand Up @@ -210,7 +211,7 @@ val apiModule = module {

val viewModelModule = module {
viewModel { LoginViewModel(get(), get(), get()) }
viewModel { EventsViewModel(get(), get(), get(), get(), get(), get()) }
viewModel { EventsViewModel(get(), get(), get(), get(), get(), get(), get()) }
viewModel { ProfileViewModel(get(), get()) }
viewModel { SignUpViewModel(get(), get(), get()) }
viewModel { EventDetailsViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
Expand Down Expand Up @@ -249,6 +250,16 @@ val networkModule = module {
objectMapper
}

single {
PagedList
.Config
.Builder()
.setPageSize(5)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we are requesting for more data after 5 elements right? I guess this is too less. We can load 15-20 events at time if it doesn't take too much time to load.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is fine. Much better for mobile data. Will test locally

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iamareebjamal did you test? This PR looks good otherwise, let's finalise it

.setInitialLoadSizeHint(5)
.setEnablePlaceholders(false)
.build()
}

single {
val connectTimeout = 15 // 15s
val readTimeout = 15 // 15s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ interface EventApi {

@GET("events/{eventId}/speakers-call")
fun getSpeakerCallForEvent(@Path("eventId") id: Long): Single<SpeakersCall>

@GET("events?include=event-sub-topic,event-topic,event-type")
fun searchEventsPaged(
@Query("sort") sort: String,
@Query("filter") eventName: String,
@Query("page[number]") page: Int,
@Query("page[size]") pageSize: Int = 5
): Single<List<Event>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,6 @@ class EventService(
private val speakersCallDao: SpeakersCallDao
) {

fun getEvents(): Flowable<List<Event>> {
val eventsFlowable = eventDao.getAllEvents()
return eventsFlowable.switchMap {
if (it.isNotEmpty())
eventsFlowable
else
eventApi.getEvents()
.map {
eventDao.insertEvents(it)
eventTopicsDao.insertEventTopics(getEventTopicList(it))
}
.toFlowable()
.flatMap {
eventsFlowable
}
}
}

fun getEventLocations(): Single<List<EventLocation>> {
return eventLocationApi.getEventLocation()
}
Expand Down Expand Up @@ -88,6 +70,14 @@ class EventService(
}
}

fun getEventsByLocationPaged(locationName: String?, page: Int): Flowable<List<Event>> {
val query = "[{\"name\":\"location-name\",\"op\":\"ilike\",\"val\":\"%$locationName%\"}," +
"{\"name\":\"ends-at\",\"op\":\"ge\",\"val\":\"%${EventUtils.getTimeInISO8601(Date())}%\"}]"
return eventApi.searchEventsPaged("name", query, page).flatMapPublisher { apiList ->
updateFavorites(apiList)
}
}

private fun updateFavorites(apiList: List<Event>): Flowable<List<Event>> {

val ids = apiList.map { it.id }.toList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import kotlinx.android.synthetic.main.content_no_internet.view.noInternetCard
import kotlinx.android.synthetic.main.content_no_internet.view.retry
import kotlinx.android.synthetic.main.fragment_events.view.eventsRecycler
import kotlinx.android.synthetic.main.fragment_events.view.locationTextView
import kotlinx.android.synthetic.main.fragment_events.view.progressBar
import kotlinx.android.synthetic.main.fragment_events.view.shimmerEvents
import kotlinx.android.synthetic.main.fragment_events.view.eventsEmptyView
import kotlinx.android.synthetic.main.fragment_events.view.emptyEventsText
Expand All @@ -37,7 +36,6 @@ import org.fossasia.openevent.general.data.Preference
import org.fossasia.openevent.general.search.location.SAVED_LOCATION
import org.fossasia.openevent.general.utils.extensions.nonNull
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
import org.fossasia.openevent.general.utils.Utils.setToolbar
import org.fossasia.openevent.general.utils.extensions.setPostponeSharedElementTransition
import org.fossasia.openevent.general.utils.extensions.setStartPostponedEnterTransition
Expand Down Expand Up @@ -67,8 +65,6 @@ class EventsFragment : Fragment(), BottomIconDoubleClick {
}
setToolbar(activity, show = false)

rootView.progressBar.isIndeterminate = true

rootView.eventsRecycler.layoutManager =
GridLayoutManager(activity, resources.getInteger(R.integer.events_column_count))

Expand All @@ -84,30 +80,24 @@ class EventsFragment : Fragment(), BottomIconDoubleClick {
handleNotificationDotVisibility(it)
})

eventsViewModel.showShimmerEvents
.nonNull()
.observe(viewLifecycleOwner, Observer { shouldShowShimmer ->
if (shouldShowShimmer) {
rootView.shimmerEvents.startShimmer()
eventsListAdapter.clear()
} else {
rootView.shimmerEvents.stopShimmer()
}
rootView.shimmerEvents.isVisible = shouldShowShimmer
})

eventsViewModel.events
eventsViewModel.pagedEvents
.nonNull()
.observe(this, Observer { list ->
eventsListAdapter.submitList(list)
showEmptyMessage(list.size)
Timber.d("Fetched events of size %s", eventsListAdapter.itemCount)
})

eventsViewModel.progress
.nonNull()
.observe(viewLifecycleOwner, Observer {
rootView.swiperefresh.isRefreshing = it
if (it) {
rootView.shimmerEvents.startShimmer()
showEmptyMessage(false)
} else {
rootView.shimmerEvents.stopShimmer()
rootView.swiperefresh.isRefreshing = false
showEmptyMessage(eventsListAdapter.currentList?.isEmpty() ?: true)
}
rootView.shimmerEvents.isVisible = it
})

eventsViewModel.error
Expand All @@ -117,6 +107,11 @@ class EventsFragment : Fragment(), BottomIconDoubleClick {
})

eventsViewModel.loadLocation()
if (rootView.locationTextView.text == getString(R.string.enter_location)) {
rootView.emptyEventsText.text = getString(R.string.choose_preferred_location_message)
} else {
rootView.emptyEventsText.text = getString(R.string.no_events_message)
}
rootView.locationTextView.text = eventsViewModel.savedLocation.value
rootView.toolbar.title = rootView.locationTextView.text

Expand All @@ -131,10 +126,10 @@ class EventsFragment : Fragment(), BottomIconDoubleClick {
eventsViewModel.connection
.nonNull()
.observe(viewLifecycleOwner, Observer { isConnected ->
if (isConnected && eventsViewModel.events.value == null) {
if (isConnected && eventsViewModel.pagedEvents.value == null) {
eventsViewModel.loadLocationEvents()
}
showNoInternetScreen(!isConnected && eventsViewModel.events.value == null)
showNoInternetScreen(!isConnected && eventsViewModel.pagedEvents.value == null)
})

rootView.locationTextView.setOnClickListener {
Expand Down Expand Up @@ -247,17 +242,8 @@ class EventsFragment : Fragment(), BottomIconDoubleClick {
rootView.noInternetCard.isVisible = show
}

private fun showEmptyMessage(itemCount: Int) {
if (itemCount == 0) {
rootView.eventsEmptyView.visibility = View.VISIBLE
if (rootView.locationTextView.text == getString(R.string.enter_location)) {
rootView.emptyEventsText.text = getString(R.string.choose_preferred_location_message)
} else {
rootView.emptyEventsText.text = getString(R.string.no_events_message)
}
} else {
rootView.eventsEmptyView.visibility = View.GONE
}
private fun showEmptyMessage(show: Boolean) {
rootView.eventsEmptyView.isVisible = show
}

override fun doubleClick() = rootView.scrollView.smoothScrollTo(0, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.fossasia.openevent.general.event

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.paging.PagedListAdapter
import org.fossasia.openevent.general.common.EventClickListener
import org.fossasia.openevent.general.common.EventsDiffCallback
import org.fossasia.openevent.general.common.FavoriteFabClickListener
Expand All @@ -15,7 +15,7 @@ import org.fossasia.openevent.general.databinding.ItemCardEventsBinding
* @property onEventClick The callback to be invoked when an event is clicked
* @property onFavFabClick The callback to be invoked when the favorite FAB is clicked
*/
class EventsListAdapter : ListAdapter<Event, EventViewHolder>(EventsDiffCallback()) {
class EventsListAdapter : PagedListAdapter<Event, EventViewHolder>(EventsDiffCallback()) {

var onEventClick: EventClickListener? = null
var onFavFabClick: FavoriteFabClickListener? = null
Expand All @@ -28,19 +28,20 @@ class EventsListAdapter : ListAdapter<Event, EventViewHolder>(EventsDiffCallback

override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
val event = getItem(position)
holder.apply {
bind(event, position)
eventClickListener = onEventClick
favFabClickListener = onFavFabClick
hashTagClickListAdapter = onHashtagClick
}
if (event != null)
holder.apply {
bind(event, position)
eventClickListener = onEventClick
favFabClickListener = onFavFabClick
hashTagClickListAdapter = onHashtagClick
}
}

/**
* The function to call when the adapter has to be cleared of items
*/
fun clear() {
this.submitList(emptyList())
this.submitList(null)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package org.fossasia.openevent.general.event

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.paging.PagedList
import androidx.paging.RxPagedListBuilder
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign
import io.reactivex.schedulers.Schedulers
import org.fossasia.openevent.general.R
import org.fossasia.openevent.general.auth.AuthHolder
import org.fossasia.openevent.general.common.SingleLiveEvent
import org.fossasia.openevent.general.connectivity.MutableConnectionLiveData
import org.fossasia.openevent.general.data.Preference
import org.fossasia.openevent.general.data.Resource
import org.fossasia.openevent.general.event.paging.EventsDataSourceFactory
import org.fossasia.openevent.general.notification.NotificationService
import org.fossasia.openevent.general.search.location.SAVED_LOCATION
import org.fossasia.openevent.general.utils.extensions.withDefaultSchedulers
Expand All @@ -24,25 +30,25 @@ class EventsViewModel(
private val resource: Resource,
private val mutableConnectionLiveData: MutableConnectionLiveData,
private val authHolder: AuthHolder,
private val notificationService: NotificationService
private val notificationService: NotificationService,
private val config: PagedList.Config
) : ViewModel() {

private val compositeDisposable = CompositeDisposable()

val connection: LiveData<Boolean> = mutableConnectionLiveData
private val mutableProgress = MutableLiveData<Boolean>()
val progress: LiveData<Boolean> = mutableProgress
val mutableNewNotifications = MutableLiveData<Boolean>()
val newNotifications: LiveData<Boolean> = mutableNewNotifications
private val mutableEvents = MutableLiveData<List<Event>>()
val events: LiveData<List<Event>> = mutableEvents
private val mutableProgress = MediatorLiveData<Boolean>()
val progress: MediatorLiveData<Boolean> = mutableProgress
private val mutablePagedEvents = MutableLiveData<PagedList<Event>>()
val pagedEvents: LiveData<PagedList<Event>> = mutablePagedEvents
private val mutableError = SingleLiveEvent<String>()
val error: LiveData<String> = mutableError
private val mutableShowShimmerEvents = MutableLiveData<Boolean>()
val showShimmerEvents: LiveData<Boolean> = mutableShowShimmerEvents
var lastSearch = ""
private val mutableSavedLocation = MutableLiveData<String>()
val savedLocation: LiveData<String> = mutableSavedLocation
private lateinit var sourceFactory: EventsDataSourceFactory

fun isLoggedIn() = authHolder.isLoggedIn()

Expand All @@ -57,57 +63,43 @@ class EventsViewModel(
if (mutableSavedLocation.value == null) return

if (lastSearch != savedLocation.value) {
compositeDisposable += eventService.getEventsByLocation(mutableSavedLocation.value)
.withDefaultSchedulers()
sourceFactory = EventsDataSourceFactory(
compositeDisposable,
eventService,
mutableSavedLocation.value,
mutableProgress
)
val eventPagedList = RxPagedListBuilder(sourceFactory, config)
.setFetchScheduler(Schedulers.io())
.buildObservable()
.cache()

compositeDisposable += eventPagedList
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.distinctUntilChanged()
.doOnSubscribe {
mutableShowShimmerEvents.value = true
}
.doFinally {
stopLoaders()
mutableProgress.value = true
}.subscribe({
stopLoaders()
mutableEvents.value = it
mutablePagedEvents.value = it
}, {
stopLoaders()
Timber.e(it, "Error fetching events")
mutableError.value = resource.getString(R.string.error_fetching_events_message)
})
} else {
mutableProgress.value = false
}
}

private fun stopLoaders() {
mutableProgress.value = false
mutableShowShimmerEvents.value = false
lastSearch = mutableSavedLocation.value ?: ""
}
fun isConnected(): Boolean = mutableConnectionLiveData.value ?: false

fun clearEvents() {
mutableEvents.value = null
mutablePagedEvents.value = null
}

fun clearLastSearch() {
lastSearch = ""
}

fun loadEvents() {
compositeDisposable += eventService.getEvents()
.withDefaultSchedulers()
.doOnSubscribe {
mutableProgress.value = true
}.doFinally {
mutableProgress.value = false
}.subscribe({
mutableEvents.value = it
}, {
Timber.e(it, "Error fetching events")
mutableError.value = resource.getString(R.string.error_fetching_events_message)
})
}

fun setFavorite(eventId: Long, favorite: Boolean) {
compositeDisposable += eventService.setFavorite(eventId, favorite)
.withDefaultSchedulers()
Expand Down
Loading