Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 6c5a45f8 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: reuse rating formatters in list rendering paths

Before:
- each rating format call created and configured a new NumberFormat
- Compose list items also did locale lookup inside per-item UI-state mapping

After:
- the formatter is created once per relevant UI scope and reused for visible items
- per-item work is reduced to formatting the value, not rebuilding formatter state each time

Why this was done:
- rating formatting happens during list rendering and rebinding
- repeated NumberFormat allocation is avoidable work
- caching at UI scope keeps the optimization local and avoids a shared global NumberFormat, which would be risky because it is mutable and not thread-safe
parent 385e3747
Loading
Loading
Loading
Loading
+19 −1
Original line number Diff line number Diff line
@@ -59,6 +59,7 @@ import foundation.e.apps.ui.utils.disableInstallButton
import foundation.e.apps.ui.utils.enableInstallButton
import kotlinx.coroutines.launch
import timber.log.Timber
import java.text.NumberFormat
import java.util.Locale
import javax.inject.Singleton
import foundation.e.elib.R as eR
@@ -75,6 +76,8 @@ class ApplicationListRVAdapter(
) : ListAdapter<Application, ApplicationListRVAdapter.ViewHolder>(ConciseAppDiffUtils()) {

    private var optionalCategory = ""
    private var ratingFormatterLocale: Locale? = null
    private var ratingFormatter: NumberFormat? = null

    private val shimmer = Shimmer.ColorHighlightBuilder()
        .setDuration(SHIMMER_DURATION_MS)
@@ -217,11 +220,26 @@ class ApplicationListRVAdapter(

        val locale = ConfigurationCompat.getLocales(root.context.resources.configuration).get(0)
            ?: Locale.getDefault()
        val formattedRating = AppUserRatingFormatter.format(searchApp.ratings.usageQualityScore, locale)
        val formattedRating = AppUserRatingFormatter.format(
            searchApp.ratings.usageQualityScore,
            getRatingFormatter(locale)
        )

        appRating.text = formattedRating ?: root.context.getString(R.string.not_available)
    }

    private fun getRatingFormatter(locale: Locale): NumberFormat {
        val currentFormatter = ratingFormatter
        if (ratingFormatterLocale == locale && currentFormatter != null) {
            return currentFormatter
        }

        return AppUserRatingFormatter.newFormatter(locale).also {
            ratingFormatterLocale = locale
            ratingFormatter = it
        }
    }

    private fun ApplicationListItemBinding.updateSourceTag(searchApp: Application) {
        sourceTag.visibility = View.INVISIBLE
        val tag = searchApp.source.toString(root.context::getString)
+21 −5
Original line number Diff line number Diff line
@@ -296,6 +296,20 @@ private fun PagingPlayStoreResultList(

    val hasLoadedCurrentQuery = remember(searchVersion) { mutableStateOf(false) }

    val notAvailableMessage = stringResource(R.string.not_available)

    val locale = LocalConfiguration.current.let { configuration ->
        ConfigurationCompat.getLocales(configuration).get(0) ?: Locale.getDefault()
    }

    val ratingFormatter = remember(locale) { AppUserRatingFormatter.newFormatter(locale) }

    val formatRating = remember(ratingFormatter, notAvailableMessage) {
        fun(rating: Double?): String {
            return AppUserRatingFormatter.format(rating, ratingFormatter) ?: notAvailableMessage
        }
    }

    LaunchedEffect(searchVersion, refreshState, lazyItems.itemCount) {
        if (refreshState is LoadState.NotLoading && lazyItems.itemCount > 0) {
            hasLoadedCurrentQuery.value = true
@@ -350,7 +364,8 @@ private fun PagingPlayStoreResultList(
                        val application = lazyItems[index]
                        if (application != null) {
                            val uiState = application.toSearchResultUiState(
                                installButtonStateProvider(application)
                                buttonState = installButtonStateProvider(application),
                                formatRating = formatRating,
                            )
                            SearchResultListItem(
                                application = application,
@@ -538,7 +553,10 @@ private fun PagingSearchResultList(
}

@Composable
private fun Application.toSearchResultUiState(buttonState: InstallButtonState): SearchResultListItemState {
private fun Application.toSearchResultUiState(
    buttonState: InstallButtonState,
    formatRating: ((Double?) -> String)? = null,
): SearchResultListItemState {
    if (isPlaceHolder) {
        return SearchResultListItemState(
            author = "",
@@ -556,11 +574,9 @@ private fun Application.toSearchResultUiState(buttonState: InstallButtonState):
        )
    }

    val locale = ConfigurationCompat.getLocales(LocalConfiguration.current).get(0)
        ?: Locale.getDefault()
    val ratingText = when {
        source == Source.OPEN_SOURCE || source == Source.PWA || isSystemApp -> ""
        else -> AppUserRatingFormatter.format(ratings.usageQualityScore, locale)
        else -> formatRating?.invoke(ratings.usageQualityScore)
            ?: stringResource(R.string.not_available)
    }

+12 −5
Original line number Diff line number Diff line
@@ -32,14 +32,21 @@ object AppUserRatingFormatter {
        return BigDecimal.valueOf(validRating).setScale(1, RoundingMode.HALF_UP).toDouble()
    }

    fun format(rating: Double?, locale: Locale): String? {
        val roundedRating = roundedNumericValue(rating) ?: return null

        return NumberFormat.getNumberInstance(locale).apply {
    fun newFormatter(locale: Locale): NumberFormat =
        NumberFormat.getNumberInstance(locale).apply {
            minimumFractionDigits = 0
            maximumFractionDigits = 1
            isGroupingUsed = false
            roundingMode = RoundingMode.HALF_UP
        }.format(roundedRating)
        }

    fun format(rating: Double?, formatter: NumberFormat): String? {
        val roundedRating = roundedNumericValue(rating) ?: return null

        return formatter.format(roundedRating)
    }

    fun format(rating: Double?, locale: Locale): String? {
        return format(rating, newFormatter(locale))
    }
}
+8 −0
Original line number Diff line number Diff line
@@ -75,4 +75,12 @@ class AppUserRatingFormatterTest {
        assertEquals("3,4", AppUserRatingFormatter.format(3.35, franceLocale))
        assertEquals("3,6", AppUserRatingFormatter.format(3.6, franceLocale))
    }

    @Test
    fun format_reusesProvidedFormatterAcrossCalls() {
        val formatter = AppUserRatingFormatter.newFormatter(usLocale)

        assertEquals("4.4", AppUserRatingFormatter.format(4.4, formatter))
        assertEquals("3.6", AppUserRatingFormatter.format(3.6, formatter))
    }
}