Author: Adam Evyčędo <git@apiote.xyz>
notify only about changed items in lists use DiffUtil to calculate changes in RecyclerViews
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt b/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt index 1dafc8cacf61a3b221df3fd18941b1e9a567089c..5a1468c35018ec56820e55c0129b1983e8aaf534 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/api/Structs.kt @@ -11,6 +11,11 @@ import org.yaml.snakeyaml.Yaml import xyz.apiote.bimba.czwek.api.structs.VehicleStatusV1 import xyz.apiote.fruchtfleisch.Reader import java.io.InputStream +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.ZoneId +import java.time.ZonedDateTime import kotlin.reflect.KClass class TrafficFormatException(override val message: String) : IllegalArgumentException() @@ -195,6 +200,16 @@ reader.readI8(), reader.readString() ) } + } + + fun toDateTime(): ZonedDateTime { + return ZonedDateTime.of( + LocalDateTime.of( + LocalDate.now().plusDays(DayOffset.toLong()), + LocalTime.of(Hour.toInt(), Minute.toInt(), Second.toInt()) + ), + ZoneId.of(Zone) + ) } } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt index a14f717b8f64732d422520d96d8930ad0dd6e9ca..9a17d965b1b7fdfe1eb3dc8669aff6e9ac3d7421 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/departures/Departures.kt @@ -14,6 +14,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -28,6 +29,9 @@ import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.dpToPixelI import xyz.apiote.bimba.czwek.repo.Departure import xyz.apiote.bimba.czwek.repo.Vehicle +import java.time.Duration +import java.time.ZoneId +import java.time.ZonedDateTime import java.util.* @@ -71,6 +75,30 @@ private var departures: List, private val onClickListener: ((Departure) -> Unit) ) : RecyclerView.Adapter<BimbaDepartureViewHolder>() { + private var lastUpdate: ZonedDateTime = + ZonedDateTime.of(0, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault()) + + inner class DiffUtilCallback( + private val oldDepartures: List<Departure>, + private val newDepartures: List<Departure> + ) : DiffUtil.Callback() { + override fun getOldListSize() = oldDepartures.size + + override fun getNewListSize() = newDepartures.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldDepartures[oldItemPosition].ID == newDepartures[newItemPosition].ID + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldDeparture = oldDepartures[oldItemPosition] + val newDeparture = newDepartures[newItemPosition] + return oldDeparture.vehicle.Line == newDeparture.vehicle.Line && oldDeparture.vehicle.Headsign == newDeparture.vehicle.Headsign && + Duration.between(lastUpdate, oldDeparture.time.toDateTime()) == Duration.between( + ZonedDateTime.now(), + oldDeparture.time.toDateTime() + ) + } + } private var departuresPositions: MutableMap<String, Int> = HashMap() @@ -100,16 +128,17 @@ departures[position] } } - @SuppressLint("NotifyDataSetChanged") // todo [3.2] DiffUtil fun update(departures: List<Departure>) { val newPositions: MutableMap<String, Int> = HashMap() departures.forEachIndexed { i, departure -> newPositions[departure.ID] = i } + val diff = DiffUtil.calculateDiff(DiffUtilCallback(this.departures, departures)) this.departures = departures departuresPositions = newPositions - notifyDataSetChanged() + lastUpdate = ZonedDateTime.now() + diff.dispatchUpdatesTo(this) } } @@ -266,7 +295,7 @@ content.apply { findViewById<MapView>(R.id.map).let { map -> map.setTileSource(TileSourceFactory.MAPNIK) if (((context?.resources?.configuration?.uiMode ?: UI_MODE_NIGHT_UNDEFINED) - and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES + and UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES ) { map.overlayManager.tilesOverlay.setColorFilter(TilesOverlay.INVERT_COLORS) } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt index 50b8d5ba35251105301364b3f2a27eef9804c80b..c59ee9c8f563016082f1d8fa69b1f379cd2e8e30 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt @@ -4,7 +4,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later package xyz.apiote.bimba.czwek.search -import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.view.LayoutInflater @@ -12,6 +11,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import xyz.apiote.bimba.czwek.R import xyz.apiote.bimba.czwek.departures.DeparturesActivity @@ -105,7 +105,7 @@ R.string.line_headsigns, line.headsigns[0].joinToString { it }, line.headsigns[1].joinToString { it }) } - holder?.description?.contentDescription =if (line.headsigns.size == 1) { + holder?.description?.contentDescription = if (line.headsigns.size == 1) { context?.getString( R.string.line_headsign_content_description, line.headsigns[0].joinToString { it }) @@ -126,6 +126,66 @@ private val context: Context?, private var queryables: List<Queryable>, ) : RecyclerView.Adapter<BimbaViewHolder>() { + class DiffUtilCallback( + private val oldQueryables: List<Queryable>, + private val newQueryables: List<Queryable> + ) : DiffUtil.Callback() { + override fun getOldListSize() = oldQueryables.size + + override fun getNewListSize() = newQueryables.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldQueryable = oldQueryables[oldItemPosition] + val newQueryable = newQueryables[newItemPosition] + return if (oldQueryable::class.java != newQueryable::class.java) { + false + } else { + when (oldQueryable) { + is Line -> { + assert(newQueryable is Line) + oldQueryable.name == (newQueryable as Line).name // TODO compare line.ID when struct is updated + } + + is Stop -> { + assert(newQueryable is Stop) + oldQueryable.code == (newQueryable as Stop).code + } + + else -> false // XXX unreachable + } + } + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldQueryable = oldQueryables[oldItemPosition] + val newQueryable = newQueryables[newItemPosition] + return when (oldQueryable) { + is Line -> { + assert(newQueryable is Line) + val oldHeadsigns = + oldQueryable.headsigns.joinToString { hsList -> hsList.joinToString { it } } + val newHeadsigns = + (newQueryable as Line).headsigns.joinToString { hsList -> hsList.joinToString { it } } + + oldQueryable.name == newQueryable.name && oldQueryable.type == newQueryable.type && + oldQueryable.colour == newQueryable.colour && oldHeadsigns == newHeadsigns + } + + is Stop -> { + assert(newQueryable is Stop) + val oldChangeOptions = + oldQueryable.changeOptions.joinToString { "${it.line}->${it.headsign}" } + val newChangeOptions = + (newQueryable as Stop).changeOptions.joinToString { "${it.line}->${it.headsign}" } + oldQueryable.name == newQueryable.name && oldChangeOptions == newChangeOptions + } + + else -> false // XXX unreachable + } + } + + } + private val onClickListener: ((Queryable) -> Unit) = { when (it) { is Stop -> { @@ -158,10 +218,11 @@ } override fun getItemCount(): Int = queryables.size - @SuppressLint("NotifyDataSetChanged") // todo [3.2] DiffUtil fun update(queryables: List<Queryable>?) { - this.queryables = queryables ?: emptyList() - notifyDataSetChanged() + val newQueryables = queryables ?: emptyList() + val diff = DiffUtil.calculateDiff(DiffUtilCallback(this.queryables, newQueryables)) + this.queryables = newQueryables + diff.dispatchUpdatesTo(this) } fun click(position: Int) { diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt index 0ba9ada8523ff57245310e2197ac5d2476485d1f..e12daa5c56560c84de23ba21bc3cd3ef267ef5b1 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/settings/feeds/FeedInfos.kt @@ -4,7 +4,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later package xyz.apiote.bimba.czwek.settings.feeds -import android.annotation.SuppressLint import android.content.Context import android.content.DialogInterface import android.os.Bundle @@ -12,6 +11,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.divider.MaterialDivider @@ -74,6 +74,29 @@ private val onClickListener: ((String) -> Unit), private val onEnabledChangedListener: ((String, Boolean) -> Unit) ) : RecyclerView.Adapter<BimbaFeedInfoViewHolder>() { + + class DiffUtilCallback( + private val oldFeeds: List<FeedInfo>, + private val oldSettings: FeedsSettings, + private val newFeeds: List<FeedInfo>, + private val newSettings: FeedsSettings + ) : DiffUtil.Callback() { + override fun getOldListSize() = oldFeeds.size + + override fun getNewListSize() = newFeeds.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldFeeds[oldItemPosition].id == newFeeds[newItemPosition].id + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldFeeds[oldItemPosition] + val oldSetting = oldSettings.settings[oldItem.id] + val newItem = newFeeds[newItemPosition] + val newSetting = newSettings.settings[newItem.id] + return oldItem.cached == newItem.cached && oldItem.name == newItem.name && oldSetting?.enabled == newSetting?.enabled + } + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BimbaFeedInfoViewHolder { val rowView = inflater.inflate(R.layout.feedinfo, parent, false) return BimbaFeedInfoViewHolder(rowView) @@ -97,8 +120,14 @@ fun getItemPosition(feedID: String): Int { return feeds.indexOfFirst { it.id == feedID } } - @SuppressLint("NotifyDataSetChanged") // todo [3.2] DiffUtil fun update(items: List<FeedInfo>?, settings: FeedsSettings?, notify: Boolean) { + val diffCallback = DiffUtilCallback( + feeds, + feedsSettings, + items?.sortedBy { it.name } ?: feeds, + settings ?: feedsSettings + ) + val diff = DiffUtil.calculateDiff(diffCallback) if (items != null) { feeds = items.sortedBy { it.name } } @@ -106,7 +135,7 @@ if (settings != null) { feedsSettings = settings } if (notify) { - notifyDataSetChanged() + diff.dispatchUpdatesTo(this) } } }