Jetpack Compose - UI not updating when flow is changed - kotlin

I have the following Composable view and view model. The problem I'm having is that even though isLoading is being updated in the view model, it's not updating the view at all, it's stuck on the LoadingUi.
#Composable
fun MediaDetailsPage(
mediaId: Long?,
viewModel: DetailsViewModel
) {
LaunchedEffect(Unit, block = {
viewModel.fetchDetailsOf(mediaId!!)
})
val isLoading by viewModel.isLoading.collectAsState()
val isError by viewModel.error.collectAsState()
when {
isLoading -> LoadingUi()
isError || viewModel.model == null -> ErrorUi()
else -> MediaDetailsUi(viewModel.model!!)
}
}
class MovieDetailsViewModel(
private val useCase: MovieDetailsUseCase = config.movieDetailsUseCase
) : DetailsViewModel() {
override fun fetchDetailsOf(id: Long, dispatcher: CoroutineDispatcher){
if(model != null) return
fetchFrom({ useCase.getMovieDetailsOf(id) }, dispatcher)
}
}
abstract class DetailsViewModel : ViewModel() {
private val _isLoading = MutableStateFlow(true)
val isLoading: StateFlow<Boolean> = _isLoading
private val _error = MutableStateFlow(false)
val error: StateFlow<Boolean> = _error
var model: MediaDetailsModel? = null
abstract fun fetchDetailsOf(id: Long, dispatcher: CoroutineDispatcher = Dispatchers.IO)
protected fun fetchFrom(
useCaseCall: suspend () -> Either<MediaDetails?, ErrorEntity?>,
dispatcher: CoroutineDispatcher
) {
job( {
_isLoading.value = true
val result = useCaseCall.invoke()
if (result.isSuccess) model = result.body!!.convert()
else _error.value = true
_isLoading.value = false
}, dispatcher)
}
}
The most confusing thing is that I'm doing pretty much identically this elsewhere in the app and that is working perfectly. I was under the impression that a composable view refreshes when a flow variable is updated, or have I got the wrong end of the stick here?

Related

My response data return " kotlinx.coroutines.flow.SafeFlow#1493a74"

I am trying to do dictionary app using kotlin language. I built the project with mvvm and clean architecture. I have been trying to pull vocabulary information from the internet using jsoap. I am using flow for data. I couldnt find where the issiue is. Normally, the words should appear on the screen or I should be able to see the data when I println on the console.But I can't see it on the screen or on the console, probably because the data coming from the internet is as follows.
kotlinx.coroutines.flow.SafeFlow#1493a74
I am sharing my codes below
ExtractedData
data class ExtractedData(
var id :Int = 0,
var word:String = "",
var meaning :String = ""
)
I created ExtractedData class to represent vocabulary or word data from internet
WordInfoRepositoryImpl
class WordInfoRepositoryImpl #Inject constructor(
private val api:DictionaryApi
) : WordInfoRepository {
//get words with meanings on the internet using jsoap
override fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>> = flow {
emit(Resource.Loading())
val listData = mutableListOf<ExtractedData>()
try {
val url = "https://ielts.com.au/australia/prepare/article-100-new-english-words-and-phrases-updated-2020"
val doc = withContext(Dispatchers.IO){
Jsoup.connect(url).get()//-->Here it gives the following warning even though I have it in withContext `Inappropriate blocking method call`
}
val table = doc.select("table")
val rows = table.select("tr")
val eventsSize = rows.size
for (i in 1 until eventsSize) {
val row = rows[i]
val cols = row.select("td")
val word = cols[0].text()
val meaning = cols[1].text()
listData.add(ExtractedData(i,word,meaning))
}
}
catch (e: IOException) {
emit(Resource.Error("IO Exception"))
}
catch (e : HttpException) {
emit(Resource.Error("HTTP EXCEPTION"))
}
emit(Resource.Success(listData))
}
}
getEventsList is in my WordInfoRepositoryImpl class in my data layer here I am pulling data from internet using jsoap
WordInfoRepository
interface WordInfoRepository {
fun getEventsList(): Flow<Resource<MutableList<ExtractedData>>>
}
this is the interface that I reference wordInforepositoryImpl in the data layer in my interface domain layer
GetWordsAndMeaningsOnTheInternetUseCase
class GetWordsAndMeaningsOnTheInternetUseCase#Inject constructor(
private val repository: WordInfoRepository
){
operator fun invoke() : Flow<Resource<MutableList<ExtractedData>>> {
return repository.getEventsList()
}
}
GetWordsAndMeaningsOnTheInternetUseCase is my usecase in my domain layer
ViewModel
#HiltViewModel
class MostUsedWordScreenViewModel #Inject constructor(
private val getWordsAndMeaningsOnTheInternetUseCase: GetWordsAndMeaningsOnTheInternetUseCase
) : ViewModel() {
private var searchJob: Job? = null
private val _state = mutableStateOf(MostUsedWordState())
val state: State<MostUsedWordState> = _state
init {
fetchData()
}
private fun fetchData() {
searchJob?.cancel()
searchJob = viewModelScope.launch(IO) {
getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = true
)
}
}
}
}
}
}
MostUsedWordScreen
#Composable
fun MostUsedWordScreen(viewModel: MostUsedWordScreenViewModel = hiltViewModel()) {
val state = viewModel.state.value
println("state --- >>> "+state.mostWordUsedItems)
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(state.mostWordUsedItems.size) { i ->
val wordInfo = state.mostWordUsedItems[i]
if(i > 0) {
Spacer(modifier = Modifier.height(8.dp))
}
MostUsedWordItem(word = wordInfo)
if(i < state.mostWordUsedItems.size - 1) {
Divider()
}
}
}
}
#Composable
fun MostUsedWordItem(word : ExtractedData ) {
// println("this is MostUsedWordItem")
Column(modifier = Modifier
.padding(5.dp)
.fillMaxWidth()) {
Text(text = word.word,
modifier = Modifier.padding(3.dp),
textAlign = TextAlign.Center,
fontSize = 18.sp,
)
}
}
It is included in the MostUsedWordScreenViewModel and MostUsedWordScreen presententation layer
Where I println("state --- >>> "+state.mostWordUsedItems) in MostUsedWordScreen, the state console shows as empty like this System.out: state --- >>> []
I tried to explain as detailed as I can, I hope you can understand.
A Flow doesn't do anything until you call a terminal operator on it. You called onEach, which is not a terminal operator. You should use collect. Or you can avoid the nesting inside a launch block by using onEach and launchIn, which does the same thing as launching a coroutine and calling collect() on the flow. You don't need to specify Dispatchers.IO here because nothing in your Flow is blocking. You correctly wrapped the blocking call in withContext(Dispatchers.IO), and the warning is a false positive. That's a well-known bug in their compiler inspection.
searchJob = getWordsAndMeaningsOnTheInternetUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Error -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = false
)
}
is Resource.Loading -> {
_state.value = state.value.copy(
mostWordUsedItems = result.data ?: mutableListOf(),
isLoading = true
)
}
}
}.launchIn(viewModelScope)
By the way, you need to move your emit(Success...) inside your try block. The way it is now, when there is an error, the error will immediately get replaced by a Success with empty list.
Side note, I recommend avoiding passing MutableLists around between classes. You have no need for them and it's a code smell. Sharing mutable state between classes is error-prone. I don't think there is any justification for using a Flow<MutableList> instead of a Flow<List>.
You rarely even need a MutableList locally in a function. For example, you could have done in your try block:
val listData = List(eventsSize - 1) {
val row = rows[it + 1]
val cols = row.select("td")
val word = cols[0].text()
val meaning = cols[1].text()
ExtractedData(i,word,meaning)
}
emit(Resource.Success(listData))

Expose value from SharedPreferences as Flow

I'm trying to get a display scaling feature to work with JetPack Compose. I have a ViewModel that exposes a shared preferences value as a flow, but it's definitely incorrect, as you can see below:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val _densityFactor: MutableStateFlow<Float> = MutableStateFlow(1.0f)
val densityFactor: StateFlow<Float>
get() = _densityFactor.asStateFlow()
private fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
// This is what I look at and go, "this is really bad."
private fun densityFactorFlow(): Flow<Float> = flow {
while (true) {
emit(getDensityFactorFromSharedPrefs())
}
}
init {
viewModelScope.launch(Dispatchers.IO) {
densityFactorFlow().collectLatest {
_densityFactor.emit(it)
}
}
}
}
Here's my Composable:
#Composable
fun MyPageRoot(
modifier: Modifier = Modifier,
viewModel: MyViewModel = hiltViewModel()
) {
val densityFactor by viewModel.densityFactor.collectAsState(initial = 1.0f)
CompositionLocalProvider(
LocalDensity provides Density(
density = LocalDensity.current.density * densityFactor
)
) {
// Content
}
}
And here's a slider that I want to slide with my finger to set the display scaling (the slider is outside the content from the MyPageRoot and will not change size on screen while the user is using the slider).
#Composable
fun ScreenDensitySetting(
modifier: Modifier = Modifier,
viewModel: SliderViewModel = hiltViewModel()
) {
var sliderValue by remember { mutableStateOf(viewModel.getDensityFactorFromSharedPrefs()) }
Text(
text = "Zoom"
)
Slider(
value = sliderValue,
onValueChange = { sliderValue = it },
onValueChangeFinished = { viewModel.setDisplayDensity(sliderValue) },
enabled = true,
valueRange = 0.5f..2.0f,
steps = 5,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colors.secondary,
activeTrackColor = MaterialTheme.colors.secondary
)
)
}
The slider composable has its own viewmodel
#HiltViewModel
class PersonalizationMenuViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
fun setDisplayDensity(density: Float) {
viewModelScope.launch {
val sharedPreference = context.getSharedPreferences(
"MEAL_ASSEMBLY_PREFS",
Context.MODE_PRIVATE
)
val editor = sharedPreference.edit()
editor.putFloat("density", density)
editor.apply()
}
}
}
I know that I need to move all the shared prefs code into a single class. But how would I write the flow such that it pulled from shared prefs when the value changed? I feel like I need a listener of some sort, but very new to Android development.
Your comment is right, that's really bad. :) You should create a OnSharedPreferenceChangeListener so it reacts to changes instead of locking up the CPU to constantly check it preemptively.
There's callbackFlow for converting listeners into Flows. You can use it like this:
fun SharedPreferences.getFloatFlowForKey(keyForFloat: String) = callbackFlow<Float> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (keyForFloat == key) {
trySend(getFloat(key, 0f))
}
}
registerOnSharedPreferenceChangeListener(listener)
if (contains(key)) {
send(getFloat(key, 0f)) // if you want to emit an initial pre-existing value
}
awaitClose { unregisterOnSharedPreferenceChangeListener(listener) }
}.buffer(Channel.UNLIMITED) // so trySend never fails
Then your ViewModel becomes:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
val densityFactor: StateFlow<Float> = sharedPreferences
.getFloatFlowForKey("density")
.stateIn(viewModelScope, SharingStarted.Eagerly, 1.0f)
}

Will the operation of collect from Flow cost many system resources when I use Compose with Kotlin?

The SoundViewModel is a ViewModel class, and val listSoundRecordState may be used by some modules in the App.
In Code A, I invoke fun collectListSoundRecord() when I need to use the data listSoundRecordState. But fun collectListSoundRecord() may be launched again and again because of Jetpack Compose recomposition, I don't know if it will cost many system resources?
In Code B, I launch private fun collectListSoundRecord() in init { }, collectListSoundRecord() will be launched only one time, but it will persist in memory until the App code closed even if I needn't to use the data listSoundRecordState, will the way cost many system resources?
Code A
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
private val _listSoundRecordState = MutableStateFlow<Result<List<MRecord>>>(Result.Loading)
val listSoundRecordState = _listSoundRecordState.asStateFlow()
init { }
//It may be launched again and again
fun collectListSoundRecord(){
viewModelScope.launch {
listRecord().collect {
result -> _listSoundRecordState.value =result
}
}
}
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
Code B
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
private val _listSoundRecordState = MutableStateFlow<Result<List<MRecord>>>(Result.Loading)
val listSoundRecordState = _listSoundRecordState.asStateFlow()
init { collectListSoundRecord() }
private fun collectListSoundRecord(){
viewModelScope.launch {
listRecord().collect {
result -> _listSoundRecordState.value =result
}
}
}
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
You would probably benefit from collecting the original flow (from listRecord()) only when there is a subscriber to your intermediate flow (the one you keep in your SoundViewModel) and cache the results.
A subscriber, in your case, would be a Composable function that collects the values (and may recompose often).
You can achieve this using the non-suspending variant of stateIn(), since you have a default value.
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
val listSoundRecordState = listRecord().stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Result.Loading)
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
In order to use the StateFlow from the UI layer (a #Composable function), you will have to transform it into a State, like so:
val viewModel: SoundViewModel = viewModel()
val listSoundRecord = viewModel.listSoundRecordState.collectAsState()
I use the below implementation of composable with view-model using LaunchedEffect
PokemonDetailScreen.kt
#Composable
fun PokemonDetailScreen(
dominantColor: Color,
pokemonName: String,
navController: NavController,
topPadding: Dp = 20.dp,
pokemonImageSize: Dp = 200.dp,
viewModel: PokemonDetailVm = hiltViewModel()
) {
var pokemonDetailData by remember { mutableStateOf<PokemonDetailView>(PokemonDetailView.DisplayLoadingView) }
val pokemonDetailScope = rememberCoroutineScope()
LaunchedEffect(key1 = true){
viewModel.getPokemonDetails(pokemonName)
viewModel.state.collect{ pokemonDetailData = it }
}
Box(
modifier = Modifier
.fillMaxSize()
.background(dominantColor)
.padding(bottom = 16.dp)
) {
PokemonHeader(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.2f)
.align(Alignment.TopCenter)
) {
navController.popBackStack()
}
PokemonBody(
pokemonInfo = pokemonDetailData,
topPadding = topPadding,
pokemonImageSize = pokemonImageSize
) {
pokemonDetailScope.launch {
viewModel.getPokemonDetails(pokemonName)
}
}
Box(
contentAlignment = Alignment.TopCenter,
modifier = Modifier
.fillMaxSize()
) {
if(pokemonDetailData is PokemonDetailView.DisplayPokemonView){
val data = (pokemonDetailData as PokemonDetailView.DisplayPokemonView).data
data.sprites.let {
// Image is available
val url = PokemonUtils.formatPokemonDetailUrl(it.frontDefault)
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
contentDescription = data.name,
contentScale = ContentScale.Fit,
modifier = Modifier
// Set the default size passed to the composable
.size(pokemonImageSize)
// Shift the image down from the top
.offset(y = topPadding)
)
}
}
}
}
}
PokemonDetailVm.kt
#HiltViewModel
class PokemonDetailVm #Inject constructor(
private val repository: PokemonRepositoryFeature
): ViewModel(){
private val _state = MutableSharedFlow<PokemonDetailView>()
val state = _state.asSharedFlow()
suspend fun getPokemonDetails(pokemonName:String) {
viewModelScope.launch {
when(val pokemonInfo = repository.getPokemonInfo(pokemonName)){
is Resource.Error -> {
pokemonInfo.message?.let {
_state.emit(PokemonDetailView.DisplayErrorView(message = it))
}
}
is Resource.Loading -> {
_state.emit(PokemonDetailView.DisplayLoadingView)
}
is Resource.Success -> {
pokemonInfo.data?.let {
_state.emit(PokemonDetailView.DisplayPokemonView(data = it))
}
}
}
}
}
}

Can I use mutableStateOf() instead of produceState in Compose?

The Code A is from the official sample project.
I don't think the produceState is necessary, so I think I can replace Code A with Code B, is it right?
BTW, the Code B can run.
Code A
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
...
}
else -> { onErrorLoading() }
}
}
Code B
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
var uiState by remember {mutableStateOf(DetailsUiState(isLoading = true))}
val cityDetailsResult = viewModel.cityDetails
uiState = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
when {
uiState.cityDetails != null -> {
DetailsContent(uiState.cityDetails!!, modifier.fillMaxSize())
}
uiState.isLoading -> {
...
}
else -> { onErrorLoading() }
}
}
Let's take a look at productState under the hood:
#Composable
fun <T> produceState(
initialValue: T,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(Unit) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
productState without a key in it's arguments, uses LaunchedEffect with Unit key which Create an effect that matches the lifecycle of the call site.
It means if DetailsScreen recomposes, the code that provides uiState won't start again.
But In code B, you are just remembering DetailsUiState across recomposition, and below line will be executed in every recomposition.
val cityDetailsResult = viewModel.cityDetails
uiState = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}

Method param require nothing instead of typed param

There's superclass with typed parameter:
abstract inner class CalendarViewHolder<T : CalendarItem>(view: View) :
RecyclerView.ViewHolder(view) {
open fun bindData(data: T) {}
}
I expect that the method bindData will accept params which are objects of CalendarItem subclasses.
But in onBindViewHolder I got the following error:
Type mismatch.
Required:
Nothing
Found:
CalendarItem
Here's the implementation of the method:
override fun onBindViewHolder(holder: CalendarViewHolder<out CalendarItem>, position: Int) {
val item = items[position]
holder.bindData(item)
if (item is DayItem) {
if (holder is OnClickStrategy) {
holder.onClickListener = {
selectedDayOfYear = item.date?.get(Calendar.DAY_OF_YEAR) ?: 0
notifyItemChanged(position)
if (previousClickedPosition != -1) {
notifyItemChanged(previousClickedPosition)
}
previousClickedPosition = holder.adapterPosition
}
}
}
}
Here's the full code of adapter:
class CalendarAdapter :
RecyclerView.Adapter<CalendarAdapter.CalendarViewHolder<out CalendarItem>>() {
private val items = mutableListOf<CalendarItem>()
private var selectedDayOfYear = -1
private var previousClickedPosition = -1
abstract inner class CalendarViewHolder<T : CalendarItem>(view: View) :
RecyclerView.ViewHolder(view) {
open fun bindData(data: T) {}
}
abstract inner class CalendarDateViewHolder(view: View) : CalendarViewHolder<DayItem>(view) {
protected lateinit var tvDate: AppCompatTextView
protected lateinit var tvMonth: AppCompatTextView
protected lateinit var tvDayDescription: AppCompatTextView
protected lateinit var clRoot: ConstraintLayout
#CallSuper
override fun bindData(data: DayItem) {
tvDate = itemView.findViewById(R.id.tv_day_number)
tvMonth = itemView.findViewById(R.id.tv_month)
tvDayDescription = itemView.findViewById(R.id.tv_day_description)
clRoot = itemView.findViewById(R.id.cl_root)
}
}
interface OnClickStrategy {
var onClickListener: () -> Unit
}
inner class CalendarEmptyViewHolder(view: View) : CalendarViewHolder<EmptyDayItem>(view)
inner class CalendarWithDateViewHolder(view: View) :
CalendarDateViewHolder(view), OnClickStrategy {
override lateinit var onClickListener: () -> Unit
override fun bindData(data: DayItem) {
super.bindData(data)
tvDate.text = data.date?.get(Calendar.DAY_OF_MONTH)?.toString()
clRoot.setOnClickListener {
onClickListener.invoke()
}
clRoot.background = ContextCompat.getDrawable(
itemView.context,
when (selectedDayOfYear) {
data.date?.get(Calendar.DAY_OF_YEAR) -> R.drawable.item_selected_background
else -> R.drawable.item_default_background
}
)
tvDate.setTextColor(
ContextCompat.getColor(
itemView.context,
if (selectedDayOfYear == data.date?.get(Calendar.DAY_OF_YEAR)) R.color.white
else R.color.black
)
)
}
}
inner class CalendarTodayDateViewHolder(view: View) : CalendarDateViewHolder(view) {
override fun bindData(data: DayItem) {
super.bindData(data)
tvDayDescription.text = "hoy"
tvDate.text = data.date?.get(Calendar.DAY_OF_MONTH)?.toString()
clRoot.background = ContextCompat.getDrawable(
itemView.context,
R.drawable.item_today_background
)
val monthName = when (data.date?.get(Calendar.MONTH)) {
0 -> "January"
1 -> "February"
else -> "March"
}
tvMonth.text = monthName
tvDate.setTextColor(ContextCompat.getColor(itemView.context, R.color.white))
tvDayDescription.setTextColor(ContextCompat.getColor(itemView.context, R.color.white))
tvMonth.setTextColor(ContextCompat.getColor(itemView.context, R.color.white))
}
}
inner class CalendarNewMonthDateViewHolder(view: View) : CalendarDateViewHolder(view),
OnClickStrategy {
override lateinit var onClickListener: () -> Unit
override fun bindData(data: DayItem) {
super.bindData(data)
tvDate.text = data.date?.get(Calendar.DAY_OF_MONTH)?.toString()
val monthName = when (data.date?.get(Calendar.MONTH)) {
0 -> "January"
1 -> "February"
else -> "March"
}
tvMonth.text = monthName
clRoot.setOnClickListener {
onClickListener.invoke()
}
tvDate.setTextColor(
ContextCompat.getColor(
itemView.context,
if (selectedDayOfYear == data.date?.get(Calendar.DAY_OF_YEAR)) R.color.white
else R.color.black
)
)
clRoot.background = ContextCompat.getDrawable(
itemView.context,
when (selectedDayOfYear) {
data.date?.get(Calendar.DAY_OF_YEAR) -> R.drawable.item_selected_background
else -> R.drawable.item_default_background
}
)
}
}
inner class CalendarDisabledDateViewHolder(view: View) : CalendarDateViewHolder(view) {
override fun bindData(data: DayItem) {
super.bindData(data)
tvDate.text = data.date?.get(Calendar.DAY_OF_MONTH)?.toString()
clRoot.background = ContextCompat.getDrawable(
itemView.context,
R.drawable.item_disabled_background
)
tvDate.setTextColor(ContextCompat.getColor(itemView.context, R.color.silver))
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = when (viewType) {
CALENDAR_EMPTY_ITEM_TYPE -> onCreateCalendarEmptyViewHolder(parent)
CALENDAR_TODAY_DATE_ITEM_TYPE -> onCreateCalendarTodayDateViewHolder(parent)
CALENDAR_NEW_MONTH_DATE_ITEM_TYPE -> onCreateCalendarNewMonthDateViewHolder(parent)
CALENDAR_WITH_DATE_ITEM_TYPE -> onCreateCalendarWithDateViewHolder(parent)
else -> onCreateCalendarDisabledDateViewHolder(parent)
}
override fun getItemViewType(position: Int) = when (val item = items[position]) {
EmptyDayItem -> CALENDAR_EMPTY_ITEM_TYPE
is DayItem -> when {
item.isToday() -> CALENDAR_TODAY_DATE_ITEM_TYPE
item.isNewMonth() -> CALENDAR_NEW_MONTH_DATE_ITEM_TYPE
item.isDateEnabled -> CALENDAR_WITH_DATE_ITEM_TYPE
else -> CALENDAR_DISABLED_DATE_ITEM_TYPE
}
else -> UNKNOWN_ITEM_TYPE
}
private fun onCreateCalendarWithDateViewHolder(parent: ViewGroup) = CalendarWithDateViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_calendar_day, parent, false)
)
private fun onCreateCalendarEmptyViewHolder(parent: ViewGroup): CalendarViewHolder<EmptyDayItem> =
CalendarEmptyViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_calendar_empty, parent, false)
)
private fun onCreateCalendarTodayDateViewHolder(parent: ViewGroup) =
CalendarTodayDateViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_calendar_day, parent, false)
)
private fun onCreateCalendarNewMonthDateViewHolder(parent: ViewGroup) =
CalendarNewMonthDateViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_calendar_day, parent, false)
)
private fun onCreateCalendarDisabledDateViewHolder(parent: ViewGroup) =
CalendarDisabledDateViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_calendar_day, parent, false)
)
override fun onBindViewHolder(holder: CalendarViewHolder<out CalendarItem>, position: Int) {
val item = items[position]
holder.bindData(item)
if (item is DayItem) {
if (holder is OnClickStrategy) {
holder.onClickListener = {
selectedDayOfYear = item.date?.get(Calendar.DAY_OF_YEAR) ?: 0
notifyItemChanged(position)
if (previousClickedPosition != -1) {
notifyItemChanged(previousClickedPosition)
}
previousClickedPosition = holder.adapterPosition
}
}
}
}
override fun getItemCount() = items.size
fun setItems(items: MutableList<DayItem>) {
val localItems: MutableList<CalendarItem> = items.toMutableList()
if (!items.isNullOrEmpty()) {
val firstDayNumber = items[0].date?.get(Calendar.DAY_OF_WEEK) ?: 0
for (i in 1 until firstDayNumber) {
localItems.add(0, EmptyDayItem)
}
this.items.clear()
this.items.addAll(localItems)
}
}
companion object {
private const val CALENDAR_EMPTY_ITEM_TYPE = 0
private const val CALENDAR_TODAY_DATE_ITEM_TYPE = 1
private const val CALENDAR_NEW_MONTH_DATE_ITEM_TYPE = 2
private const val CALENDAR_WITH_DATE_ITEM_TYPE = 3
private const val CALENDAR_DISABLED_DATE_ITEM_TYPE = 4
private const val CALENDAR_HEADER_ITEM_TYPE = 5
private const val UNKNOWN_ITEM_TYPE = -1
}
}
sealed class CalendarItem
class DayItem(
val date: Calendar? = null,
val isDateEnabled: Boolean = true
): CalendarItem() {
fun isToday() = if (date != null) isSameDay(date, Calendar.getInstance()) else false
fun isNewMonth() = date?.get(Calendar.DAY_OF_MONTH) == 1
private fun isSameDay(cal1: Calendar, cal2: Calendar) =
cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&
cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)
}
object EmptyDayItem : CalendarItem()
What's the reason of the problem?
You specified the type as out CalendarItem, but the bind function consumes the item of type T, so it is in an in position, not out. So by marking it as out at the class declaration, you have restricted it to Nothing wherever T appears as a function parameter type.
There's no way to make generics work for you here, as I'm assuming you're trying to get it to allow you to bind items without casting. onCreateViewHolder has your ViewHolder in an out position, which means your particular ViewHolder cannot have invariant or contravariant (in) type if you want to be able to return different types of your ViewHolder subclass from onCreateViewHolder.
It's just not logically possible for generics to safely allow you to pass these items to the appropriate types, and you will have to use casting.
I suggest you eliminate the generic type from your abstract ViewHolder, and make each subclass throw an error if it gets the wrong type. Then you will quickly get exceptions when you test your app if there are any type mismatches and you will know the problem lies in either getItemViewType or onCreateViewHolder, where you've hooked up your classes and types incorrectly.
I also suggest using View Binding, because it will make these classes much more streamlined. No lateinit view properties or findViewById calls.
For example:
abstract class CalendarViewHolder(view: View): RecyclerView.ViewHolder(view) {
abstract fun bindData(data: CalendarItem): Any
// Any return type here allows subclasses to use "= with(binding)"
// to reduce nesting.
}
class CalendarDateViewHolder(private val binding: ItemCalendarDayBinding): MyViewHolder(binding.root) {
override fun bind(item: CalendarItem) = with(binding) {
item as? DayItem ?: error("Wrong item type.")
// item is now smart cast as DayItem
// bind the DayItem data to the views.
// can access the views directly by name in with block.
}
}
private fun onCreateCalendarWithDateViewHolder(parent: ViewGroup) = CalendarWithDateViewHolder(
ItemCalendarDayBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
And I suggest also getting rid of all those onCreate functions and adding constructors that use the parent ViewGroup, just to streamline things, for example:
class CalendarDateViewHolder private constructor(private val binding: ItemCalendarDayBinding): MyViewHolder(binding.root) {
constructor(parent: ViewGroup): this(ItemCalendarDayBinding.inflate(LayoutInflater.from(parent.context), parent, false))
//...
}