0

Only a few months into Kotlin development, so apologies. Been over all the previous threads and nothing has helped with this issue.

Whenever a character is selected and the Firebase DB is updated, the RecyclerView is reset to the top. I want it to retain its position. I don't have a solid understanding of how the parts work yet to know what is causing it to reset.

Activity:

class DailiesActivity : AppCompatActivity(), CharacterAdapter.CharacterItemListener {

private lateinit var binding : ActivityDailiesBinding
private lateinit var auth : FirebaseAuth
var db = FirebaseFirestore.getInstance()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityDailiesBinding.inflate(layoutInflater)
    setContentView(binding.root)

    auth = Firebase.auth

    val recyclerView = findViewById<RecyclerView>(R.id.charactersRecyclerView)
    val viewModel : CharacterViewModel by viewModels()
    viewModel.getCharacters().observe(this) {
        binding.charactersRecyclerView.adapter = CharacterAdapter(this, it, this, recyclerView)
    }

    setSupportActionBar(binding.mainToolBar.toolbar)
    supportActionBar?.title = ""
}

/**
 * Nav Setup
 */
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menuInflater.inflate(R.menu.main_menu, menu)
    return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
        R.id.action_to_home -> { startActivity(Intent(applicationContext, MainActivity::class.java)) }
        R.id.action_to_critters -> { startActivity(Intent(applicationContext, CrittersActivity::class.java)) }
        R.id.action_to_recipes -> { startActivity(Intent(applicationContext, RecipesActivity::class.java)) }
        R.id.action_to_settings -> { startActivity(Intent(applicationContext, SettingsActivity::class.java)) }
    }

    return super.onOptionsItemSelected(item)
}

// updating firebase data when character is selected
// Firebase documentation: https://firebase.google.com/docs/firestore/manage-data/add-data
override fun characterSelected(character: Character, adapterPosition: Int) {
    Log.i("Character_Selected", "$character")

    val uID = auth.currentUser?.uid
    val currentCharacter = uID?.let { db.collection(it).document(character.name + "") }

    if (!character.spoken) {
        currentCharacter?.update("spoken", true)
    }
    else {
        currentCharacter?.update("spoken", false)
    }
    binding.charactersRecyclerView.adapter?.notifyItemChanged(adapterPosition)
}

Adapter:

class CharacterAdapter(val context : Context,
                   private val characters : List<Character>,
                   private val itemListener : CharacterItemListener,
                   private val recyclerView: RecyclerView ) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {

inner class CharacterViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView){
    var characterImageView = itemView.findViewById<ImageView>(R.id.characterImageView)
    val characterTextView = itemView.findViewById<TextView>(R.id.characterTextView)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
    val inflater = LayoutInflater.from(parent.context)
    val view = inflater.inflate(R.layout.item_character, parent, false)
    return CharacterViewHolder(view)
}

override fun onBindViewHolder(viewHolder: CharacterViewHolder, position: Int) {
    val character = characters[position]

    with (viewHolder) {
        // when expression documentation: https://kotlinlang.org/docs/control-flow.html#when-expression
        // handling dynamic ImageViews: https://stackoverflow.com/questions/16906528/change-image-of-imageview-programmatically-in-android
        // images courtesy of: https://dreamlightvalleywiki.com/Characters
        when (character.name) {
            "Anna" -> characterImageView.setBackgroundResource(R.drawable.anna)
            "Ariel" -> characterImageView.setBackgroundResource(R.drawable.ariel)
            "Buzz" -> characterImageView.setBackgroundResource(R.drawable.buzz)
            "Donald Duck" -> characterImageView.setBackgroundResource(R.drawable.donald)
            "Elsa" -> characterImageView.setBackgroundResource(R.drawable.elsa)
            "Eric" -> characterImageView.setBackgroundResource(R.drawable.eric)
            "Goofy" -> characterImageView.setBackgroundResource(R.drawable.goofy)
            "Kristoff" -> characterImageView.setBackgroundResource(R.drawable.kristoff)
            "Maui" -> characterImageView.setBackgroundResource(R.drawable.maui)
            "Merlin" -> characterImageView.setBackgroundResource(R.drawable.merlin)
            "Mickey Mouse" -> characterImageView.setBackgroundResource(R.drawable.mickey)
            "Minnie Mouse" -> characterImageView.setBackgroundResource(R.drawable.minnie)
            "Mirabel" -> characterImageView.setBackgroundResource(R.drawable.mirabel)
            "Moana" -> characterImageView.setBackgroundResource(R.drawable.moana)
            "Mother Gothel" -> characterImageView.setBackgroundResource(R.drawable.gothel)
            "Olaf" -> characterImageView.setBackgroundResource(R.drawable.olaf)
            "Remy" -> characterImageView.setBackgroundResource(R.drawable.remy)
            "Scar" -> characterImageView.setBackgroundResource(R.drawable.scar)
            "Scrooge" -> characterImageView.setBackgroundResource(R.drawable.scrooge)
            "Stitch" -> characterImageView.setBackgroundResource(R.drawable.stitch)
            "Ursula" -> characterImageView.setBackgroundResource(R.drawable.ursula)
            "WALL-E" -> characterImageView.setBackgroundResource(R.drawable.walle)
            "Woody" -> characterImageView.setBackgroundResource(R.drawable.woody)

            else -> {
                characterImageView = null
            }
        }
        characterTextView.text = character.name

        // retains 'spoken to' background colour if user navigates to different page and returns
        if (character.spoken) {
            itemView.setBackgroundResource(R.drawable.background_selected)
        }
        else {
            itemView.setBackgroundResource(R.drawable.background)
        }

        // when pressed, background changes and updates value in list
        itemView.setOnClickListener {
            itemListener.characterSelected(character, viewHolder.adapterPosition)
            character.spoken = !character.spoken
            if (character.spoken) {
                itemView.setBackgroundResource(R.drawable.background_selected)
            } else {
                itemView.setBackgroundResource(R.drawable.background)
            }
        }
    }
}

override fun getItemCount(): Int {
    return characters.size
}

interface CharacterItemListener {
    fun characterSelected(character: Character, adapterPosition: Int)
}

XML:

<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="5dp"
android:background="@color/purple_200"
tools:context=".DailiesActivity">

<!-- navigation -->
<include
    android:id="@+id/mainToolBar"
    layout="@layout/toolbar_main"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    />

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/daily_discussions"
    android:textSize="25sp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.045"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.615" />

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/charactersRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
    app:spanCount="3"
    android:paddingTop="10dp"
    app:layout_constraintTop_toBottomOf="@id/textView"
    app:layout_constraintBottom_toTopOf="@id/mainToolBar"
    app:layout_constraintStart_toEndOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

ViewModel:

class CharacterViewModel : ViewModel() {
private var characters = MutableLiveData<List<Character>>()

init {
    val userID = Firebase.auth.currentUser?.uid

    val db = userID?.let {
        FirebaseFirestore.getInstance().collection(it)
            .whereEqualTo("type", "character")
            .addSnapshotListener{ documents, exception ->
                if (exception != null) {
                    Log.w("DB_Response", "Listen failed ${exception.code}")
                    return@addSnapshotListener
                }
                documents?.let {
                    val charactersList = ArrayList<Character>()
                    for (document in documents) {
                        Log.i("DB_Response", "${document.data}")
                        val character = document.toObject(Character::class.java)
                        charactersList.add(character)
                    }
                    characters.value = charactersList
                }
            }
    }
}

fun getCharacters() : LiveData<List<Character>> {
    return characters
}

2 Answers2

0
viewModel.getCharacters().observe(this) {
    binding.charactersRecyclerView.adapter = CharacterAdapter(this, it, this, recyclerView)
}

You are setting the adapter each time the data is updated. You only need to set the adapter once in your onCreate() method.

Also an observation. If you are using binding in our app you do not need to use the "findViewById" method. You can just manage your views like this after you inflated the layout:

binding.apply{
    charactersRecyclerView.adapter = CharacterAdapter(this, viewModel.characterList, this, recyclerView)
}

Your list is preserved in the View Model durring the activity lifecycle so no worries here.

Purple6666
  • 26
  • 4
  • Thank you! using findViewById was a remnant of trying to get it to work. Regarding setting the adapter only once, how would I edit it to function that way? – Dragamaroon Apr 13 '23 at 16:23
  • @Dragamaroon you create the adapter once when the hosting Activity/Fragment is created (as part of setting up the RecyclerView). Then ideally, your adapter will have some kind of `setData` function where you can pass in the current data - that's what you'd call when your Firebase stuff provides an update. That `setData` function would have to handle updating the adapter's internal data and notifying about what's changed – cactustictacs Apr 13 '23 at 18:56
  • The adapter's `notify*` functions take care of redisplaying things, but they behave differently. `notifyDataSetChanged` causes a total refresh because it has no info besides "something changed" whereas e.g. `notifyItemRemoved` gives it specific info about *what* has changed, so it doesn't need to redraw the whole thing and can even do animations. Since you're getting *"a bunch of data"* from elsewhere, you probably want to use `DiffUtil` [https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil] which takes care of comparing old and new data and notifying specific changes – cactustictacs Apr 13 '23 at 18:59
0

Can you paste your CharacterViewModel code?

But I can see some stuff that doesn't look correct.

  1. Move
val viewModel : CharacterViewModel by viewModels()

to the top of onCreate to make it global. In this way, you'll have access to it outside of onCreate function.

  1. Declare your Adapter only once instead of instantiating it in every call on getCharacters() observer.

I can't confirm without checking your viewModel code, but my guess is everytime you update your DB on characterSelected function, it triggers the viewModel.getCharacters() and then refresh the whole list instantiating a new adapter in the observer.

David Melo
  • 31
  • 3
  • Thanks, I've added the ViewModel code. To clarify, I should move: `val viewModel : CharacterViewModel by viewModels()` above the onCreate? – Dragamaroon Apr 13 '23 at 16:20
  • As I suspected, you have a listener on your viewModel that is listening to every change in your DB, overriding the value of **characters**, triggering a new event on **getCharacters()** and recreating the adapter instance with the list every time. It's like an endless cycle. If you wanna just get the values from the DB at the beginning, don't register a listener to it, just fetch the data once instead. – David Melo Apr 13 '23 at 19:40