Skip to content
Open
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
425 changes: 305 additions & 120 deletions README.md

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.compose.compiler)
}

Expand All @@ -24,10 +23,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}

kotlin {
jvmToolchain(17)
}

buildFeatures {
compose = true
}
Expand All @@ -38,6 +33,10 @@ android {
}
}

kotlin {
jvmToolchain(17)
}

dependencies {

// library dependency
Expand Down
17 changes: 17 additions & 0 deletions appupdater/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# AndroidAppUpdater - AppUpdater (XML/View) module consumer ProGuard rules
# These rules are bundled with the library and applied to consumer projects.

# Keep the public DialogFragment entry point
-keep class com.pouyaheydari.appupdater.main.ui.AppUpdaterDialog { *; }

# Keep public API model classes
-keep class com.pouyaheydari.appupdater.main.ui.model.UpdaterDialogData { *; }
-keep class com.pouyaheydari.appupdater.main.ui.model.UpdaterFragmentModel { *; }

# Keep DSL builder functions and classes
-keep class com.pouyaheydari.appupdater.main.dsl.** { *; }

# Keep Parcelable CREATOR fields
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,99 @@
package com.pouyaheydari.appupdater.main.dsl

import android.graphics.Typeface
import com.pouyaheydari.appupdater.core.model.Theme
import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
import com.pouyaheydari.appupdater.main.ui.AppUpdaterDialog
import com.pouyaheydari.appupdater.main.ui.model.UpdaterDialogData
import com.pouyaheydari.appupdater.store.domain.StoreListItem
import com.pouyaheydari.appupdater.store.domain.stores.AppStore

/**
* This inline function helps building stores in DSL way
* Mutable builder for [StoreListItem] used in DSL context.
*/
inline fun store(block: StoreListItem.() -> Unit): StoreListItem = StoreListItem().apply(block)
class StoreListItemBuilder {
lateinit var store: AppStore
var title: String = ""
var icon: Int = 0

fun build(): StoreListItem = StoreListItem(store = store, title = title, icon = icon)
}
Comment on lines +14 to +20
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid lateinit in builder — use nullable with validation for clearer errors.

Same issue as the Compose version: if the user forgets to set store, the UninitializedPropertyAccessException provides poor guidance. Use a nullable property with explicit validation.

Proposed fix
 class StoreListItemBuilder {
-    lateinit var store: AppStore
+    var store: AppStore? = null
     var title: String = ""
     var icon: Int = 0

-    fun build(): StoreListItem = StoreListItem(store = store, title = title, icon = icon)
+    fun build(): StoreListItem = StoreListItem(
+        store = requireNotNull(store) { "StoreListItemBuilder requires 'store' to be set" },
+        title = title,
+        icon = icon
+    )
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class StoreListItemBuilder {
lateinit var store: AppStore
var title: String = ""
var icon: Int = 0
fun build(): StoreListItem = StoreListItem(store = store, title = title, icon = icon)
}
class StoreListItemBuilder {
var store: AppStore? = null
var title: String = ""
var icon: Int = 0
fun build(): StoreListItem = StoreListItem(
store = requireNotNull(store) { "StoreListItemBuilder requires 'store' to be set" },
title = title,
icon = icon
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@appupdater/src/main/java/com/pouyaheydari/appupdater/main/dsl/DSLUtils.kt`
around lines 14 - 20, Replace the lateinit store property in
StoreListItemBuilder with a nullable var (store: AppStore? = null) and add
explicit validation in build(): check that store is not null and throw a clear
IllegalStateException (e.g., "StoreListItemBuilder.store must be set") before
constructing StoreListItem; update the build() method to use the validated
non-null store when calling StoreListItem(store = ..., title = title, icon =
icon) so callers get a helpful error instead of
UninitializedPropertyAccessException.


/**
* Mutable builder for [DirectDownloadListItem] used in DSL context.
*/
class DirectDownloadListItemBuilder {
var title: String = ""
var url: String = ""

fun build(): DirectDownloadListItem = DirectDownloadListItem(title = title, url = url)
}

/**
* Mutable builder for [UpdaterDialogData] used in DSL context.
*/
class UpdaterDialogDataBuilder {
var title: String = ""
var description: String = ""
var storeList: List<StoreListItem> = listOf()
var directDownloadList: List<DirectDownloadListItem> = listOf()
var isForceUpdate: Boolean = false
var typeface: Typeface? = null
var errorWhileOpeningStoreCallback: ((String) -> Unit)? = null
var theme: Theme = Theme.SYSTEM_DEFAULT

fun build(): UpdaterDialogData = UpdaterDialogData(
title = title,
description = description,
storeList = storeList,
directDownloadList = directDownloadList,
isForceUpdate = isForceUpdate,
typeface = typeface,
errorWhileOpeningStoreCallback = errorWhileOpeningStoreCallback,
theme = theme,
)
}
Comment on lines 13 to +55
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting shared builders to a common module.

StoreListItemBuilder and DirectDownloadListItemBuilder are duplicated between the Compose and Fragment DSL modules. Since both build the same model classes (StoreListItem, DirectDownloadListItem), consider extracting them to a shared module (e.g., :store or a new :dsl-core) to reduce duplication and ensure consistent behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@appupdater/src/main/java/com/pouyaheydari/appupdater/main/dsl/DSLUtils.kt`
around lines 13 - 55, Extract the duplicated builder classes
StoreListItemBuilder and DirectDownloadListItemBuilder into a shared module
(e.g., :store or :dsl-core) and replace the duplicate implementations in the
Compose and Fragment DSL modules with imports from that shared module; update
the build system (module dependency) and package/imports so
UpdaterDialogDataBuilder continues to reference the shared
StoreListItemBuilder/DirectDownloadListItemBuilder types and ensure the shared
builders still produce StoreListItem and DirectDownloadListItem with the same
property names (store, title, icon, url) and build() signatures.


/**
* DSL builder for constructing a [StoreListItem].
*
* Example usage:
* ```
* val item = store {
* store = StoreFactory.getStore(AppStoreType.GOOGLE_PLAY, "com.example.app")
* title = "Google Play"
* icon = R.drawable.ic_google_play
* }
* ```
*/
inline fun store(block: StoreListItemBuilder.() -> Unit): StoreListItem =
StoreListItemBuilder().apply(block).build()

/**
* This inline function helps building direct download links in DSL way
* DSL builder for constructing a [DirectDownloadListItem].
*
* Example usage:
* ```
* val item = directDownload {
* title = "Direct APK"
* url = "https://example.com/app.apk"
* }
* ```
*/
inline fun directDownload(block: DirectDownloadListItem.() -> Unit): DirectDownloadListItem = DirectDownloadListItem().apply(block)
inline fun directDownload(block: DirectDownloadListItemBuilder.() -> Unit): DirectDownloadListItem =
DirectDownloadListItemBuilder().apply(block).build()

/**
* This inline function helps building UpdateDialog in DSL way
* DSL builder for constructing and obtaining an [AppUpdaterDialog] instance.
*
* Example usage:
* ```
* val dialog = updateDialogBuilder {
* title = "New Update Available"
* description = "Version 2.0 is ready"
* storeList = listOf(...)
* }
* ```
*/
inline fun updateDialogBuilder(block: UpdaterDialogData.() -> Unit): AppUpdaterDialog =
AppUpdaterDialog.getInstance(UpdaterDialogData().apply(block))
inline fun updateDialogBuilder(block: UpdaterDialogDataBuilder.() -> Unit): AppUpdaterDialog =
AppUpdaterDialog.getInstance(UpdaterDialogDataBuilder().apply(block).build())
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.pouyaheydari.appupdater.directdownload.data.DirectDownloadListItem
import com.pouyaheydari.appupdater.directdownload.utils.donwloadapk.checkPermissionsAndDownloadApk
import com.pouyaheydari.appupdater.directdownload.utils.downloadapk.checkPermissionsAndDownloadApk
import com.pouyaheydari.appupdater.directdownload.utils.installapk.installAPK
import com.pouyaheydari.appupdater.main.R
import com.pouyaheydari.appupdater.main.data.mapper.mapToSelectedTheme
Expand Down Expand Up @@ -62,7 +62,9 @@ class AppUpdaterDialog : DialogFragment() {
// Getting data passed to the library
val data = arguments?.parcelable(UPDATE_DIALOG_KEY) ?: UpdaterFragmentModel.EMPTY
if (data == UpdaterFragmentModel.EMPTY || (data.storeList.isEmpty() && data.directDownloadList.isEmpty())) {
throw IllegalArgumentException("Invalid data provided to the updater dialog. Either 'storeList' or 'directDownloadList' must be non-empty. For more details, refer to the documentation at $UPDATE_DIALOG_README_URL")
throw IllegalArgumentException(
"Invalid data provided to the updater dialog. Either 'storeList' or 'directDownloadList' must be non-empty. For more details, refer to the documentation at $UPDATE_DIALOG_README_URL",
)
}
setDialogBackground(mapToSelectedTheme(data.theme, requireContext()))
isCancelable = data.isForceUpdate
Expand Down Expand Up @@ -95,7 +97,19 @@ class AppUpdaterDialog : DialogFragment() {
}

private fun subscribeToViewModel(theme: UserSelectedTheme) {
viewModel.screenState.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
// Observe persistent UI state (survives config changes)
viewModel.screenState
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.onEach { uiState ->
when (uiState) {
DialogScreenUiState.Idle -> hideUpdateInProgressDialog()
DialogScreenUiState.UpdateInProgress -> showUpdateInProgressDialog(theme)
}
}.launchIn(lifecycleScope)

// Observe one-shot side-effects (consumed exactly once, no replay on config change)
viewModel.sideEffect
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.onEach {
when (it) {
is DialogScreenStates.DownloadApk -> {
Expand All @@ -105,7 +119,7 @@ class AppUpdaterDialog : DialogFragment() {
androidSdkVersion = Build.VERSION.SDK_INT,
notificationTitle = requireContext().getString(com.pouyaheydari.appupdater.directdownload.R.string.appupdater_download_notification_title),
notificationDescription = requireContext().getString(com.pouyaheydari.appupdater.directdownload.R.string.appupdater_download_notification_desc),
downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager,
) {
viewModel.handleIntent(DialogScreenIntents.OnApkDownloadStarted)
}
Expand All @@ -121,10 +135,13 @@ class AppUpdaterDialog : DialogFragment() {
viewModel.handleIntent(DialogScreenIntents.OnErrorCallbackExecuted)
}

DialogScreenStates.HideUpdateInProgress -> hideUpdateInProgressDialog()
DialogScreenStates.ShowUpdateInProgress -> showUpdateInProgressDialog(theme)
DialogScreenStates.Empty -> hideUpdateInProgressDialog()
is DialogScreenStates.InstallApk -> installDownloadedApk(it)

// Persistent states are handled by screenState collector above
DialogScreenStates.HideUpdateInProgress,
DialogScreenStates.ShowUpdateInProgress,
DialogScreenStates.Empty,
-> { /* handled by screenState */ }
Comment on lines +139 to +144
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if these DialogScreenStates variants are used anywhere else
echo "Searching for usages of HideUpdateInProgress, ShowUpdateInProgress, Empty..."
rg -n "DialogScreenStates\.(HideUpdateInProgress|ShowUpdateInProgress|Empty)" --type kotlin

Repository: HeyPouya/AndroidAppUpdater

Length of output: 569


🏁 Script executed:

fd -e kt -type f | xargs grep -l "sealed class DialogScreenStates" | head -5

Repository: HeyPouya/AndroidAppUpdater

Length of output: 240


🏁 Script executed:

fd DialogScreenStates .

Repository: HeyPouya/AndroidAppUpdater

Length of output: 160


🏁 Script executed:

cat -n ./appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/model/DialogScreenStates.kt

Repository: HeyPouya/AndroidAppUpdater

Length of output: 788


🏁 Script executed:

rg -n "(HideUpdateInProgress|ShowUpdateInProgress|Empty)" --type kotlin appupdater/src/main/java/com/pouyaheydari/appupdater/

Repository: HeyPouya/AndroidAppUpdater

Length of output: 1531


Remove unused DialogScreenStates variants from sealed interface definition.

These three variants (Empty, ShowUpdateInProgress, HideUpdateInProgress) are defined in the sealed interface but never emitted to the sideEffect channel anywhere in the codebase. They only appear in the dead code branch at lines 139–144 of AppUpdaterDialog.kt. Remove the variant definitions from DialogScreenStates.kt and the corresponding dead code branch from the when statement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterDialog.kt`
around lines 139 - 144, Remove the unused sealed variants
DialogScreenStates.Empty, DialogScreenStates.ShowUpdateInProgress, and
DialogScreenStates.HideUpdateInProgress from the sealed interface definition in
DialogScreenStates (delete their enum/variant declarations) and delete the dead
when-branch in AppUpdaterDialog.kt that matches those three cases (the branch
containing the comment "handled by screenState"). After removal, search for any
remaining references to those symbols (e.g., usages in other files or imports)
and clean them up, then run a build to ensure no unresolved references remain.

}
}.launchIn(lifecycleScope)
}
Expand Down Expand Up @@ -265,7 +282,7 @@ class AppUpdaterDialog : DialogFragment() {
storeList,
directDownloadList,
!isForceUpdate,
theme
theme,
)

TypefaceHolder.typeface = typeface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,55 @@ import com.pouyaheydari.appupdater.main.ui.model.DialogScreenIntents
import com.pouyaheydari.appupdater.main.ui.model.DialogScreenStates
import com.pouyaheydari.appupdater.main.utils.ErrorCallbackHolder
import com.pouyaheydari.appupdater.main.utils.TypefaceHolder
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

/**
* ViewModel for the app updater dialog (XML/View module).
*
* Uses [StateFlow] for persistent UI state (e.g. update-in-progress indicator) and
* [Channel] for one-shot side-effects (e.g. open store, download APK) that should
* not replay on configuration change.
*/
internal class AppUpdaterViewModel(
private val isUpdateInProgressUseCase: GetDownloadStateUseCase,
private val setDownloadStateUseCase: SetDownloadStateUseCase
private val setDownloadStateUseCase: SetDownloadStateUseCase,
) : ViewModel() {
val screenState = MutableStateFlow<DialogScreenStates>(DialogScreenStates.HideUpdateInProgress)
/** Persistent UI state that survives configuration changes. */
private val _screenState = MutableStateFlow<DialogScreenUiState>(DialogScreenUiState.Idle)
val screenState: StateFlow<DialogScreenUiState> = _screenState.asStateFlow()

/** One-shot side-effect events consumed exactly once by the UI. */
private val _sideEffect = Channel<DialogScreenStates>(Channel.BUFFERED)
val sideEffect = _sideEffect.receiveAsFlow()

fun handleIntent(intent: DialogScreenIntents) {
when (intent) {
is DialogScreenIntents.OnDirectLinkClicked -> screenState.value = DialogScreenStates.DownloadApk(intent.item.url)
is DialogScreenIntents.OnStoreClicked -> screenState.value = DialogScreenStates.OpenStore(intent.item.store)
DialogScreenIntents.OnStoreOpened -> screenState.value = DialogScreenStates.Empty
DialogScreenIntents.OnErrorCallbackExecuted -> screenState.value = DialogScreenStates.Empty
DialogScreenIntents.OnApkDownloadRequested -> screenState.value = DialogScreenStates.Empty
is DialogScreenIntents.OnDirectLinkClicked ->
_sideEffect.trySend(DialogScreenStates.DownloadApk(intent.item.url))

is DialogScreenIntents.OnStoreClicked ->
_sideEffect.trySend(DialogScreenStates.OpenStore(intent.item.store))

is DialogScreenIntents.OnOpeningStoreFailed ->
_sideEffect.trySend(DialogScreenStates.ExecuteErrorCallback(intent.store.getUserReadableName()))
Comment on lines +41 to +48
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider handling trySend result for critical side effects.

trySend() can fail silently if the channel buffer is full (returns ChannelResult.isFailure). For non-critical UI events this is typically acceptable, but if any of these side effects are critical (e.g., DownloadApk), consider logging failures or using send() within a coroutine scope.

Example: Log on failure
_sideEffect.trySend(DialogScreenStates.DownloadApk(intent.item.url))
    .onFailure { Log.w(TAG, "Failed to send DownloadApk side effect") }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@appupdater/src/main/java/com/pouyaheydari/appupdater/main/ui/AppUpdaterViewModel.kt`
around lines 41 - 48, In AppUpdaterViewModel where DialogScreenIntents are
mapped to _sideEffect.trySend (for cases like
DialogScreenIntents.OnDirectLinkClicked, OnStoreClicked, OnOpeningStoreFailed
producing DialogScreenStates.DownloadApk, OpenStore, ExecuteErrorCallback),
handle the trySend result: inspect the ChannelResult (use onFailure or check
isFailure) and either log the failure (include a TAG and context such as which
DialogScreenState failed) or switch to calling send() from a coroutine scope for
critical events (e.g., DownloadApk) so failures are not dropped silently.


DialogScreenIntents.OnApkDownloadStarted -> {
setUpdateInProgress()
observeUpdateInProgressStatus()
}

is DialogScreenIntents.OnOpeningStoreFailed ->
screenState.value = DialogScreenStates.ExecuteErrorCallback(intent.store.getUserReadableName())

DialogScreenIntents.OnApkInstallationStarted -> screenState.value = DialogScreenStates.Empty
// These intents signal the UI consumed a side-effect; no further action needed.
DialogScreenIntents.OnStoreOpened,
DialogScreenIntents.OnErrorCallbackExecuted,
DialogScreenIntents.OnApkDownloadRequested,
DialogScreenIntents.OnApkInstallationStarted,
-> { /* consumed */ }
}
}

Expand All @@ -47,12 +70,17 @@ internal class AppUpdaterViewModel(
private fun observeUpdateInProgressStatus() {
viewModelScope.launch {
isUpdateInProgressUseCase().collectLatest { downloadState ->
screenState.value = when (downloadState) {
is DownloadState.Downloaded ->
DialogScreenStates.InstallApk(downloadState.apk)
when (downloadState) {
is DownloadState.Downloaded -> {
_screenState.value = DialogScreenUiState.Idle
_sideEffect.trySend(DialogScreenStates.InstallApk(downloadState.apk))
}

is DownloadState.Downloading ->
DialogScreenStates.ShowUpdateInProgress
_screenState.value = DialogScreenUiState.UpdateInProgress

is DownloadState.Failed ->
_screenState.value = DialogScreenUiState.Idle
}
}
}
Expand All @@ -64,3 +92,15 @@ internal class AppUpdaterViewModel(
super.onCleared()
}
}

/**
* Persistent UI state for the updater dialog.
* This state survives configuration changes and is safe to replay.
*/
internal sealed interface DialogScreenUiState {
/** No download in progress; idle state. */
data object Idle : DialogScreenUiState

/** An APK download is in progress; show the progress indicator. */
data object UpdateInProgress : DialogScreenUiState
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@ internal class DirectRecyclerAdapter(
private val list: List<DirectDownloadListItem>,
private val typeface: Typeface?,
private val listener: (DirectDownloadListItem) -> Unit,
) : RecyclerView.Adapter<DirectRecyclerAdapter.SoresViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SoresViewHolder =
DownloadDirectItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.run { SoresViewHolder(this) }
) : RecyclerView.Adapter<DirectRecyclerAdapter.DirectDownloadViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DirectDownloadViewHolder =
DownloadDirectItemBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
.run { DirectDownloadViewHolder(this) }

override fun getItemCount(): Int = list.size

override fun onBindViewHolder(holder: SoresViewHolder, position: Int) = holder.onBind(list[position])
override fun onBindViewHolder(holder: DirectDownloadViewHolder, position: Int) = holder.onBind(list[position])

inner class SoresViewHolder(private val binding: DownloadDirectItemBinding) : RecyclerView.ViewHolder(binding.root) {
inner class DirectDownloadViewHolder(
private val binding: DownloadDirectItemBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun onBind(item: DirectDownloadListItem) {
val txtDirect = binding.txtDirect
txtDirect.text = item.title
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ internal class StoresRecyclerAdapter(
private val theme: UserSelectedTheme,
private val typeface: Typeface?,
private val listener: (StoreListItem) -> Unit,
) : RecyclerView.Adapter<StoresRecyclerAdapter.SoresViewHolder>() {
) : RecyclerView.Adapter<StoresRecyclerAdapter.StoresViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
DownloadStoresItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.run { SoresViewHolder(this) }
DownloadStoresItemBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
.run { StoresViewHolder(this) }

override fun getItemCount(): Int = list.size

override fun onBindViewHolder(holder: SoresViewHolder, position: Int) = holder.onBind(list[position])
override fun onBindViewHolder(holder: StoresViewHolder, position: Int) = holder.onBind(list[position])

inner class SoresViewHolder(private val binding: DownloadStoresItemBinding) : RecyclerView.ViewHolder(binding.root) {
inner class StoresViewHolder(
private val binding: DownloadStoresItemBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun onBind(item: StoreListItem) {
val txtStoreTitle = binding.txtStoreTitle
val imgStore = binding.imgStore
Expand Down
Loading