fixed crash + fixed lib + fixed preview

This commit is contained in:
Osten 2024-03-18 15:54:54 +01:00
parent 8d5b73495d
commit eb60be54ed
8 changed files with 169 additions and 114 deletions

View file

@ -12,6 +12,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import java.util.concurrent.CopyOnWriteArrayList
open class ViewHolderState<T>(val view: ViewBinding) : ViewHolder(view.root) { open class ViewHolderState<T>(val view: ViewBinding) : ViewHolder(view.root) {
open fun save(): T? = null open fun save(): T? = null
@ -27,6 +28,8 @@ class StateViewModel : ViewModel() {
val layoutManagerStates = hashMapOf<Int, HashMap<Int, Any?>>() val layoutManagerStates = hashMapOf<Int, HashMap<Int, Any?>>()
} }
abstract class NoStateAdapter<T : Any>(fragment: Fragment) : BaseAdapter<T, Any>(fragment, 0)
/** /**
* BaseAdapter is a persistent state stored adapter that supports headers and footers. * BaseAdapter is a persistent state stored adapter that supports headers and footers.
* This should be used for restoring eg scroll or focus related to a view when it is recreated. * This should be used for restoring eg scroll or focus related to a view when it is recreated.
@ -83,7 +86,8 @@ abstract class BaseAdapter<
) )
fun submitList(list: List<T>?) { fun submitList(list: List<T>?) {
mDiffer.submitList(list) // deep copy at least the top list, because otherwise adapter can go crazy
mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) })
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {

View file

@ -388,7 +388,7 @@ class HomeParentItemAdapterPreview(
viewModel.loadMoreHomeScrollResponses() viewModel.loadMoreHomeScrollResponses()
} }
} }
val item = previewAdapter.getItem(position) ?: return val item = previewAdapter.getItemOrNull(position) ?: return
onSelect(item, position) onSelect(item, position)
} }
} }
@ -516,6 +516,7 @@ class HomeParentItemAdapterPreview(
when (preview) { when (preview) {
is Resource.Success -> { is Resource.Success -> {
previewAdapter.submitList(preview.value.second) previewAdapter.submitList(preview.value.second)
previewAdapter.hasMoreItems = preview.value.first
/*if (!.setItems( /*if (!.setItems(
preview.value.second, preview.value.second,
preview.value.first preview.value.first

View file

@ -8,7 +8,7 @@ import androidx.fragment.app.Fragment
import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding
import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.NoStateAdapter
import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.ViewHolderState
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.TV
@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage
class HomeScrollAdapter( class HomeScrollAdapter(
fragment: Fragment fragment: Fragment
) : BaseAdapter<LoadResponse, Any>(fragment, "HomeScrollAdapter".hashCode()) { ) : NoStateAdapter<LoadResponse>(fragment) {
var hasMoreItems: Boolean = false var hasMoreItems: Boolean = false
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Any> { override fun onCreateContent(parent: ViewGroup): ViewHolderState<Any> {

View file

@ -53,6 +53,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.EnumSet import java.util.EnumSet
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.collections.set import kotlin.collections.set
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {
@ -125,7 +126,7 @@ class HomeViewModel : ViewModel() {
private val _resumeWatching = MutableLiveData<List<SearchResponse>>() private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>() private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
private val previewResponses = mutableListOf<LoadResponse>() private val previewResponses = CopyOnWriteArrayList<LoadResponse>()
private val previewResponsesAdded = mutableSetOf<String>() private val previewResponsesAdded = mutableSetOf<String>()
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
@ -327,7 +328,13 @@ class HomeViewModel : ViewModel() {
val filteredList = val filteredList =
context?.filterHomePageListByFilmQuality(list) ?: list context?.filterHomePageListByFilmQuality(list) ?: list
expandable[list.name] = expandable[list.name] =
ExpandableHomepageList(filteredList, 1, home.hasNext) ExpandableHomepageList(
filteredList.copy(
list = CopyOnWriteArrayList(
filteredList.list
)
), 1, home.hasNext
)
} }
} }
@ -342,8 +349,7 @@ class HomeViewModel : ViewModel() {
val currentList = val currentList =
items.shuffled().filter { it.list.isNotEmpty() } items.shuffled().filter { it.list.isNotEmpty() }
.flatMap { it.list } .flatMap { it.list }
.distinctBy { it.url } .distinctBy { it.url }.toList()
.toList()
if (currentList.isNotEmpty()) { if (currentList.isNotEmpty()) {
val randomItems = val randomItems =

View file

@ -49,12 +49,10 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity
@ -62,6 +60,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.math.abs import kotlin.math.abs
const val LIBRARY_FOLDER = "library_folder" const val LIBRARY_FOLDER = "library_folder"
@ -165,7 +164,8 @@ class LibraryFragment : Fragment() {
} }
// Set the color for the search exit icon to the correct theme text color // Set the color for the search exit icon to the correct theme text color
val searchExitIcon = binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) val searchExitIcon =
binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
val searchExitIconColor = TypedValue() val searchExitIconColor = TypedValue()
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true) activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
@ -233,7 +233,7 @@ class LibraryFragment : Fragment() {
if (listLibraryItems.isNotEmpty()) { if (listLibraryItems.isNotEmpty()) {
val listLibraryItem = listLibraryItems.random() val listLibraryItem = listLibraryItems.random()
libraryViewModel.currentSyncApi?.syncIdName?.let { libraryViewModel.currentSyncApi?.syncIdName?.let {
loadLibraryItem(it, listLibraryItem.syncId,listLibraryItem) loadLibraryItem(it, listLibraryItem.syncId, listLibraryItem)
} }
} }
} }
@ -312,44 +312,46 @@ class LibraryFragment : Fragment() {
binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
binding?.viewpager?.adapter = binding?.viewpager?.adapter = ViewpagerAdapter(
binding?.viewpager?.adapter ?: ViewpagerAdapter( fragment = this,
mutableListOf(), { isScrollingDown: Boolean ->
{ isScrollingDown: Boolean -> if (isScrollingDown) {
if (isScrollingDown) { binding?.sortFab?.shrink()
binding?.sortFab?.shrink() binding?.libraryRandom?.shrink()
binding?.libraryRandom?.shrink() } else {
} else { binding?.sortFab?.extend()
binding?.sortFab?.extend() binding?.libraryRandom?.extend()
binding?.libraryRandom?.extend() }
} }) callback@{ searchClickCallback ->
}) callback@{ searchClickCallback -> // To prevent future accidents
// To prevent future accidents debugAssert({
debugAssert({ searchClickCallback.card !is SyncAPI.LibraryItem
searchClickCallback.card !is SyncAPI.LibraryItem }, {
}, { "searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem" })
})
val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId
val syncName = val syncName =
libraryViewModel.currentSyncApi?.syncIdName ?: return@callback libraryViewModel.currentSyncApi?.syncIdName ?: return@callback
when (searchClickCallback.action) { when (searchClickCallback.action) {
SEARCH_ACTION_SHOW_METADATA -> { SEARCH_ACTION_SHOW_METADATA -> {
(activity as? MainActivity)?.loadPopup(searchClickCallback.card, load = false) (activity as? MainActivity)?.loadPopup(
searchClickCallback.card,
load = false
)
/*activity?.showPluginSelectionDialog( /*activity?.showPluginSelectionDialog(
syncId, syncId,
syncName, syncName,
searchClickCallback.card.apiName searchClickCallback.card.apiName
)*/ )*/
} }
SEARCH_ACTION_LOAD -> { SEARCH_ACTION_LOAD -> {
loadLibraryItem(syncName, syncId, searchClickCallback.card) loadLibraryItem(syncName, syncId, searchClickCallback.card)
}
} }
} }
}
binding?.apply { binding?.apply {
viewpager.offscreenPageLimit = 2 viewpager.offscreenPageLimit = 2
@ -395,7 +397,11 @@ class LibraryFragment : Fragment() {
} }
} }
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages (viewpager.adapter as? ViewpagerAdapter)?.submitList(pages.map {
it.copy(
items = CopyOnWriteArrayList(it.items)
)
})
//fix focus on the viewpager itself //fix focus on the viewpager itself
(viewpager.getChildAt(0) as RecyclerView).apply { (viewpager.getChildAt(0) as RecyclerView).apply {
tag = "tv_no_focus_tag" tag = "tv_no_focus_tag"
@ -403,10 +409,10 @@ class LibraryFragment : Fragment() {
} }
// Using notifyItemRangeChanged keeps the animations when sorting // Using notifyItemRangeChanged keeps the animations when sorting
viewpager.adapter?.notifyItemRangeChanged( /*viewpager.adapter?.notifyItemRangeChanged(
0, 0,
viewpager.adapter?.itemCount ?: 0 viewpager.adapter?.itemCount ?: 0
) )*/
libraryViewModel.currentPage.value?.let { page -> libraryViewModel.currentPage.value?.let { page ->
binding?.viewpager?.setCurrentItem(page, false) binding?.viewpager?.setCurrentItem(page, false)
@ -464,12 +470,14 @@ class LibraryFragment : Fragment() {
} }
}.attach() }.attach()
binding?.libraryTabLayout?.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener { binding?.libraryTabLayout?.addOnTabSelectedListener(object :
TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) { override fun onTabSelected(tab: TabLayout.Tab?) {
binding?.libraryTabLayout?.selectedTabPosition?.let { page -> binding?.libraryTabLayout?.selectedTabPosition?.let { page ->
libraryViewModel.switchPage(page) libraryViewModel.switchPage(page)
} }
} }
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabReselected(tab: TabLayout.Tab?) = Unit override fun onTabReselected(tab: TabLayout.Tab?) = Unit
}) })
@ -569,8 +577,9 @@ class LibraryFragment : Fragment() {
} }
@SuppressLint("NotifyDataSetChanged")
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
(binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() binding?.viewpager?.adapter?.notifyDataSetChanged()
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
} }

View file

@ -113,7 +113,7 @@ class LibraryViewModel : ViewModel() {
} }
val desiredSortingMethod = val desiredSortingMethod =
ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode) ListSorting.entries.getOrNull(DataStoreHelper.librarySortingMode)
if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) { if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) {
sort(desiredSortingMethod, null, pages) sort(desiredSortingMethod, null, pages)
} else { } else {

View file

@ -1,105 +1,123 @@
package com.lagradost.cloudstream3.ui.library package com.lagradost.cloudstream3.ui.library
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
import androidx.recyclerview.widget.RecyclerView import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView.OnFlingListener import androidx.recyclerview.widget.RecyclerView.OnFlingListener
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.BaseAdapter
import com.lagradost.cloudstream3.ui.BaseDiffCallback
import com.lagradost.cloudstream3.ui.ViewHolderState
import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) :
ViewHolderState<Bundle>(binding) {
override fun save(): Bundle =
Bundle().apply {
putParcelable(
"pageRecyclerview",
binding.pageRecyclerview.layoutManager?.onSaveInstanceState()
)
}
override fun restore(state: Bundle) {
state.getParcelable<Parcelable>("pageRecyclerview")?.let { recycle ->
binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle)
}
}
}
class ViewpagerAdapter( class ViewpagerAdapter(
var pages: List<SyncAPI.Page>, fragment: Fragment,
val scrollCallback: (isScrollingDown: Boolean) -> Unit, val scrollCallback: (isScrollingDown: Boolean) -> Unit,
val clickCallback: (SearchClickCallback) -> Unit val clickCallback: (SearchClickCallback) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : BaseAdapter<SyncAPI.Page, Bundle>(fragment,
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { id = "ViewpagerAdapter".hashCode(),
return PageViewHolder( diffCallback = BaseDiffCallback(
itemSame = { a, b ->
a.title == b.title
},
contentSame = { a, b ->
a.items == b.items && a.title == b.title
}
)) {
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Bundle> {
return ViewpagerAdapterViewHolderState(
LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false) LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
) )
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onUpdateContent(
when (holder) { holder: ViewHolderState<Bundle>,
is PageViewHolder -> { item: SyncAPI.Page,
holder.bind(pages[position], position, unbound.remove(position)) position: Int
} ) {
} val binding = holder.view
if (binding !is LibraryViewpagerPageBinding) return
(binding.pageRecyclerview.adapter as? PageAdapter)?.updateList(item.items)
} }
private val unbound = mutableSetOf<Int>() override fun onBindContent(holder: ViewHolderState<Bundle>, item: SyncAPI.Page, position: Int) {
val binding = holder.view
if (binding !is LibraryViewpagerPageBinding) return
/** binding.pageRecyclerview.tag = position
* Used to mark all pages for re-binding and forces all items to be refreshed binding.pageRecyclerview.apply {
* Without this the pages will still use the same adapters spanCount =
**/ binding.root.context.getSpanCount() ?: 3
fun rebind() { if (adapter == null) { // || rebind
unbound.addAll(0..pages.size) // Only add the items after it has been attached since the items rely on ItemWidth
this.notifyItemRangeChanged(0, pages.size) // Which is only determined after the recyclerview is attached.
} // If this fails then item height becomes 0 when there is only one item
doOnAttach {
inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : adapter = PageAdapter(
RecyclerView.ViewHolder(binding.root) { item.items.toMutableList(),
fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) { this,
binding.pageRecyclerview.tag = position clickCallback
binding.pageRecyclerview.apply { )
spanCount =
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
if (adapter == null || rebind) {
// Only add the items after it has been attached since the items rely on ItemWidth
// Which is only determined after the recyclerview is attached.
// If this fails then item height becomes 0 when there is only one item
doOnAttach {
adapter = PageAdapter(
page.items.toMutableList(),
this,
clickCallback
)
}
} else {
(adapter as? PageAdapter)?.updateList(page.items)
scrollToPosition(0)
} }
} else {
(adapter as? PageAdapter)?.updateList(item.items)
// scrollToPosition(0)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
val diff = scrollY - oldScrollY val diff = scrollY - oldScrollY
//Expand the top Appbar based on scroll direction up/down, simulate phone behavior //Expand the top Appbar based on scroll direction up/down, simulate phone behavior
if (isLayout(TV or EMULATOR)) { if (isLayout(TV or EMULATOR)) {
binding.root.rootView.findViewById<AppBarLayout>(R.id.search_bar) binding.root.rootView.findViewById<AppBarLayout>(R.id.search_bar)
.apply { .apply {
if (diff <= 0) if (diff <= 0)
setExpanded(true) setExpanded(true)
else else
setExpanded(false) setExpanded(false)
} }
}
if (diff == 0) return@setOnScrollChangeListener
scrollCallback.invoke(diff > 0)
} }
} else { if (diff == 0) return@setOnScrollChangeListener
onFlingListener = object : OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean { scrollCallback.invoke(diff > 0)
scrollCallback.invoke(velocityY > 0) }
return false } else {
} onFlingListener = object : OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
scrollCallback.invoke(velocityY > 0)
return false
} }
} }
} }
} }
} }
override fun getItemCount(): Int {
return pages.size
}
} }

View file

@ -19,6 +19,13 @@ sealed class UiText {
data class DynamicString(val value: String) : UiText() { data class DynamicString(val value: String) : UiText() {
override fun toString(): String = value override fun toString(): String = value
override fun equals(other: Any?): Boolean {
if (other !is DynamicString) return false
return this.value == other.value
}
override fun hashCode(): Int = value.hashCode()
} }
class StringResource( class StringResource(
@ -27,6 +34,16 @@ sealed class UiText {
) : UiText() { ) : UiText() {
override fun toString(): String = override fun toString(): String =
"resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}" "resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}"
override fun equals(other: Any?): Boolean {
if (other !is StringResource) return false
return this.resId == other.resId && this.args == other.args
}
override fun hashCode(): Int {
var result = resId
result = 31 * result + args.hashCode()
return result
}
} }
fun asStringNull(context: Context?): String? { fun asStringNull(context: Context?): String? {