Skip to content

Commit

Permalink
Abstract the model for showing a Phrase on the home screen
Browse files Browse the repository at this point in the history
We didn't need as much data as we were passing up, and this lets us better assert that the content hasn't changed when observing reactively, reducing flickers.
  • Loading branch information
PaulKlauser committed Jun 11, 2024
1 parent 8e72692 commit 88119ad
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 177 deletions.
2 changes: 1 addition & 1 deletion app/src/main/java/com/willowtree/vocable/AppKoinModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ val vocableKoinModule = module {
single { VocableDatabase.createVocableDatabase(get()) }
single { get<VocableDatabase>().presetPhrasesDao() }
single<VocableEnvironment> { VocableEnvironmentImpl() }
viewModel { PresetsViewModel(get(), get(), get(named<PresetsViewModel>())) }
viewModel { PresetsViewModel(get(), get(), get(named<PresetsViewModel>()), get()) }
viewModel { EditCategoriesViewModel(get()) }
viewModel { EditCategoryPhrasesViewModel(get(), get(), get()) }
viewModel { AddUpdateCategoryViewModel(get(), get(), get()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class NumberPadFragment : BaseFragment<FragmentNumberPadBinding>() {
private const val KEY_PHRASES = "KEY_PHRASES"
const val MAX_PHRASES = 12

fun newInstance(phrases: List<Phrase?>) = NumberPadFragment().apply {
fun newInstance(phrases: List<PhraseGridItem>) = NumberPadFragment().apply {
arguments = bundleOf(KEY_PHRASES to ArrayList(phrases))
}
}
Expand All @@ -37,7 +37,7 @@ class NumberPadFragment : BaseFragment<FragmentNumberPadBinding>() {
val numColumns = resources.getInteger(R.integer.number_pad_columns)
val numRows = resources.getInteger(R.integer.number_pad_rows)

val phrases = arguments?.getParcelableArrayList<Phrase>(KEY_PHRASES)
val phrases = arguments?.getParcelableArrayList<PhraseGridItem>(KEY_PHRASES)

phrases?.let {
with(binding.phrasesContainer) {
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/java/com/willowtree/vocable/presets/PhraseGridItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.willowtree.vocable.presets

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
sealed class PhraseGridItem : Parcelable {

@Parcelize
data class Phrase(
val phraseId: String,
val text: String
) : PhraseGridItem()

@Parcelize
object AddPhrase : PhraseGridItem()
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class PhrasesFragment : BaseFragment<FragmentPhrasesBinding>() {
companion object {
private const val KEY_PHRASES = "KEY_PHRASES"

fun newInstance(phrases: List<Phrase?>): PhrasesFragment {
fun newInstance(phrases: List<PhraseGridItem>): PhrasesFragment {
return PhrasesFragment().apply {
arguments = Bundle().apply {
putParcelableArrayList(KEY_PHRASES, ArrayList(phrases))
Expand All @@ -41,7 +41,7 @@ class PhrasesFragment : BaseFragment<FragmentPhrasesBinding>() {
val numColumns = resources.getInteger(R.integer.phrases_columns)
val numRows = resources.getInteger(R.integer.phrases_rows)

val phrases = arguments?.getParcelableArrayList<Phrase?>(KEY_PHRASES)
val phrases = arguments?.getParcelableArrayList<PhraseGridItem>(KEY_PHRASES)
phrases?.let {
with(binding.phrasesContainer) {
layoutManager = GridLayoutManager(requireContext(), numColumns)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ class PresetsFragment : BaseFragment<FragmentPresetsBinding>() {
}
}

private fun handlePhrases(phrases: List<Phrase?>) {
private fun handlePhrases(phrases: List<PhraseGridItem>) {
binding.emptyPhrasesText.isVisible =
phrases.isEmpty() && !recentsCategorySelected && categoriesAdapter.getSize() > 0
binding.emptyAddPhraseButton.isVisible =
Expand Down Expand Up @@ -306,9 +306,9 @@ class PresetsFragment : BaseFragment<FragmentPresetsBinding>() {
}

inner class PhrasesPagerAdapter(fm: FragmentManager) :
VocableFragmentStateAdapter<Phrase?>(fm, viewLifecycleOwner.lifecycle) {
VocableFragmentStateAdapter<PhraseGridItem>(fm, viewLifecycleOwner.lifecycle) {

override fun setItems(items: List<Phrase?>) {
override fun setItems(items: List<PhraseGridItem>) {
super.setItems(items)
setPagingButtonsEnabled(phrasesAdapter.numPages > 1)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.willowtree.vocable.ICategoriesUseCase
import com.willowtree.vocable.IPhrasesUseCase
import com.willowtree.vocable.utils.ILocalizedResourceUtility
import com.willowtree.vocable.utils.IdlingResourceContainer
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
Expand All @@ -23,7 +25,8 @@ import kotlinx.coroutines.launch
class PresetsViewModel(
private val categoriesUseCase: ICategoriesUseCase,
private val phrasesUseCase: IPhrasesUseCase,
private val idlingResourceContainer: IdlingResourceContainer
private val idlingResourceContainer: IdlingResourceContainer,
private val localizedResourceUtility: ILocalizedResourceUtility
) : ViewModel() {

val categoryList: LiveData<List<Category>> = categoriesUseCase.categories()
Expand Down Expand Up @@ -53,24 +56,30 @@ class PresetsViewModel(
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000L), null)
val selectedCategoryLiveData: LiveData<Category?> = selectedCategory.asLiveData()

val currentPhrases: LiveData<List<Phrase?>> = selectedCategoryId
val currentPhrases: LiveData<List<PhraseGridItem>> = selectedCategoryId
.filterNotNull()
.flatMapLatest { categoryId ->
phrasesUseCase.getPhrasesForCategoryFlow(categoryId).map { phrases ->
val phrasesToReturn = phrases.run {
val phraseGridItems: List<PhraseGridItem> = phrases.run {
if (categoryId != PresetCategories.RECENTS.id) {
sortedBy { it.sortOrder }
} else {
this
}
}.toMutableList<Phrase?>()
}.map {
PhraseGridItem.Phrase(
it.phraseId,
localizedResourceUtility.getTextFromPhrase(it)
)
}
if (categoryId != PresetCategories.RECENTS.id && categoryId != PresetCategories.USER_KEYPAD.id && phrases.isNotEmpty()) {
//Add null to end of normal non empty category phrase list for the "+ Add Phrase" button
phrasesToReturn.add(null)
phraseGridItems + PhraseGridItem.AddPhrase
} else {
phraseGridItems
}
phrasesToReturn
}
}
.distinctUntilChanged()
.asLiveData()

private val liveNavToAddPhrase = MutableLiveData<Boolean>()
Expand All @@ -90,10 +99,10 @@ class PresetsViewModel(
selectedCategoryId.update { categoryId }
}

fun addToRecents(phrase: Phrase) {
fun addToRecents(phraseId: String) {
viewModelScope.launch {
idlingResourceContainer.run {
phrasesUseCase.updatePhraseLastSpokenTime(phrase.phraseId)
phrasesUseCase.updatePhraseLastSpokenTime(phraseId)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,56 +8,51 @@ import androidx.recyclerview.widget.RecyclerView
import com.willowtree.vocable.R
import com.willowtree.vocable.databinding.PhraseButtonAddBinding
import com.willowtree.vocable.databinding.PhraseButtonBinding
import com.willowtree.vocable.presets.Phrase
import com.willowtree.vocable.utils.locale.LocalizedResourceUtility
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import com.willowtree.vocable.presets.PhraseGridItem
import java.util.Locale

class PhraseAdapter(
private val phrases: List<Phrase?>,
private val phrases: List<PhraseGridItem>,
private val numRows: Int,
private val phraseClickAction: ((Phrase) -> Unit)?,
private val phraseClickAction: ((String) -> Unit)?,
private val phraseAddClickAction: (() -> Unit)?
) :
RecyclerView.Adapter<PhraseAdapter.PhraseViewHolder>(), KoinComponent {
) : RecyclerView.Adapter<PhraseAdapter.PhraseViewHolder>() {

abstract inner class PhraseViewHolder(
itemView: View
) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(text: String, position: Int)
abstract fun bind(position: Int)
}

inner class PhraseItemViewHolder(itemView: View) : PhraseAdapter.PhraseViewHolder(itemView) {
inner class PhraseGridItemViewHolder(itemView: View) :
PhraseAdapter.PhraseViewHolder(itemView) {

override fun bind(text: String, position: Int) {
val binding = PhraseButtonBinding.bind(itemView)
binding.root.setText(text, Locale.getDefault())
binding.root.action = {
phrases[position]?.let { phraseClickAction?.invoke(it) }
}
}
}

inner class PhraseAddItemViewHolder(itemView: View) : PhraseAdapter.PhraseViewHolder(itemView) {
override fun bind(position: Int) {
when (val gridItem = phrases[position]) {
is PhraseGridItem.Phrase -> {
val binding = PhraseButtonBinding.bind(itemView)
binding.root.setText(gridItem.text, Locale.getDefault())
binding.root.action = {
phraseClickAction?.invoke(gridItem.phraseId)
}
}

override fun bind(text: String, position: Int) {
val binding = PhraseButtonAddBinding.bind(itemView)
binding.root.action = {
phraseAddClickAction?.invoke()
PhraseGridItem.AddPhrase -> {
val binding = PhraseButtonAddBinding.bind(itemView)
binding.root.action = {
phraseAddClickAction?.invoke()
}
}
}
}
}

private val localizedResourceUtility: LocalizedResourceUtility by inject()

private var _minHeight: Int? = null

override fun getItemViewType(position: Int): Int {
return if (phrases[position] == null) {
R.layout.phrase_button_add
} else {
R.layout.phrase_button
return when (phrases[position]) {
is PhraseGridItem.Phrase -> R.layout.phrase_button
PhraseGridItem.AddPhrase -> R.layout.phrase_button_add
}
}

Expand All @@ -74,16 +69,12 @@ class PhraseAdapter(
isInvisible = false
}
}
return if (viewType == R.layout.phrase_button_add) {
PhraseAddItemViewHolder(itemView)
} else {
PhraseItemViewHolder(itemView)
}

return PhraseGridItemViewHolder(itemView)
}

override fun onBindViewHolder(holder: PhraseAdapter.PhraseViewHolder, position: Int) {
val text = localizedResourceUtility.getTextFromPhrase(phrases[position])
holder.bind(text, position)
holder.bind(position)
}

private fun getMinHeight(parent: ViewGroup): Int {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.willowtree.vocable.utils

import com.willowtree.vocable.presets.Category
import com.willowtree.vocable.presets.Phrase

interface ILocalizedResourceUtility {
fun getTextFromCategory(category: Category?): String
fun getTextFromPhrase(phrase: Phrase?): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class LocalizedResourceUtility(
return category?.text(context) ?: ""
}

fun getTextFromPhrase(phrase: Phrase?): String {
override fun getTextFromPhrase(phrase: Phrase?): String {
return phrase?.text(context) ?: ""
}
}
Loading

0 comments on commit 88119ad

Please sign in to comment.