Link to my code: https://github.com/tylerwilbanks/word-game-android
After around maybe 10 seconds after startup, if you tap keyboard buttons quickly, suddenly the app performance will tank with 5-10 second frame processes.
The issue does not seem to be excessive re-composition, 1 composition will suddenly take a very long time whereas others just before it were very quick.
Once the slow frame begins, the application remains in this state until relaunch.
Any insights would be greatly appreciated.
MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GuessWordValidator.initValidWords(this)
viewModel.setupGame()
setContent {
WordGameTheme {
DailyWordScreen(
state = viewModel.state.collectAsStateWithLifecycle(),
onEvent = viewModel::onEvent
)
}
}
}
}
ViewModel:
class DailyWordViewModel(application: Application) : AndroidViewModel(application) {
private val _state = MutableStateFlow(DailyWordState())
private val context = getApplication<Application>().applicationContext
val state
get() = _state.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000L),
DailyWordState()
)
...
}
ViewModel.onEvent():
fun onEvent(event: DailyWordEvent) {
when (event) {
is DailyWordEvent.OnCharacterPress -> {
val currentGuess = _state.value.currentGuess
currentGuess?.getLetterForInput?.let { guessLetter ->
guessLetter.updateCharacter(event.character)
_state.update {
it.copy(
currentGuess = currentGuess,
currentWord = currentGuess.displayWord
)
}
}
}
DailyWordEvent.OnDeletePress -> {
val currentGuess = _state.value.currentGuess
currentGuess?.getLetterToErase?.let { guessLetter ->
guessLetter.updateCharacter(' ')
_state.update {
it.copy(
currentGuess = currentGuess,
currentWord = currentGuess.displayWord
)
}
}
}
...
The profiler shows the very long frames but no indicator as to why. Memory usage stays level.
I've tried updating the state variable in the viewModel
directly without using coroutines, no avail. I also tried explicitly using viewModel.launch(Dispatchers.IO) {}
to make sure I'm not holding up the main thread with too many allocated jobs.
The only thing I can think that is causing the issue is .collectAsStateWithLifecycle
in the composable is getting backed up with all the inputs and gets stuck.
I really don't know how else to send state down into the composable if collecting the StateFlow
causes issues with input.
Getting this in my logcat: Skipped 741 frames! The application may be doing too much work on its main thread.
Edit 1: There is no way that compose is getting bogged down by collecting state. I've noticed that I don't even have to spam the keyboard anymore, it will begin skipping frames after waiting around 10 seconds after application launch, and then typing a few letters at a normal pace.
Edit 2: I've noticed in the profiler that everytime i type a letter, the app's memory consumption goes up appx. 1.2 mb and never goes back down.
Edit 3: Thanks to Tenfour04, this issue has been solved! The reason for this was that my DailyWordState
object I was sending down into my composables was Mutable
. I changed this to contain a list Immutable
domain objects. My DailyWordState
was still flagged as Mutable
because List<Any>
is considered mutable
because you can convert a MutableList
to a List
. I remedied this by adding @Immutable
annotation to my DailyWordState
object. This works great now, however now my task is to learn how to structure my data without the need for the @Immutable
annotation.