2

I am having difficulties retrieving the information correctly from Firebase Firestore for my Recycler Adapter. I am not sure what I might be doing wrong but I used a Document Reference to get the required field but now it seems to just copy the same thing over and over, I want it to display each created users profile and display it on my RecyclerAdapter but am not sure what I should do and have tried different methods but get a

"No setter/field error" on my Model Class "Users".

This is my Firebase Schema This is what it is outputting

This is what I have my code as so far

[Update] This is what I have imported

import Models.User
import android.content.Intent
import android.content.res.Configuration
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.firebase.ui.firestore.FirestoreRecyclerAdapter
import com.firebase.ui.firestore.FirestoreRecyclerOptions
import com.google.android.material.navigation.NavigationView
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.*
import com.squareup.picasso.Picasso
import de.hdodenhof.circleimageview.CircleImageView
import kotlinx.android.synthetic.main.all_nearby_users.*
import kotlinx.android.synthetic.main.toolbar_layout.*

Oncreate

       auth = FirebaseAuth.getInstance()
        val customUserId = auth.currentUser!!.uid
        val db = FirebaseFirestore.getInstance()
        val userRef = db.collection("sUsers").document(customUserId)
        val userQuery = db.collection("sUsers").orderBy("Full Name", Query.Direction.DESCENDING).limit(10)

//User List Layout
        all_users_nearby_list.layoutManager = LinearLayoutManager(this)

        //Firestore
        val firestoreRecyclerOptions: FirestoreRecyclerOptions<Users> = FirestoreRecyclerOptions.Builder<Users>()
            .setQuery(userQuery, Users::class.java)
            .build()
        adapter = UserFirestoreRecyclerAdapter(firestoreRecyclerOptions)
        all_users_nearby_list.adapter = adapter

Firestore Recycler Adapter

private inner class UserFirestoreRecyclerAdapter internal constructor
        (firestoreRecyclerOptions: FirestoreRecyclerOptions<Users>): FirestoreRecyclerAdapter<Users, UserViewHolder>(firestoreRecyclerOptions) {

            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
                val userView = LayoutInflater.from(parent.context)
                    .inflate(R.layout.display_users_profile, parent, false)

                return UserViewHolder(userView)
            }

            override fun onBindViewHolder(holder: UserViewHolder, position: Int, model: Users) {

                holder.setFullname(model.fullname)
                holder.setProfileimage(model.profileImage)

            }

        }

UserViewHolder


   private inner class UserViewHolder internal constructor (private val pView: View) : RecyclerView.ViewHolder(pView) {


        internal fun setFullname(fullname: String) {
            val username = pView.findViewById<TextView>(R.id.usernameTextView)
            val db = FirebaseFirestore.getInstance()
            val docRef = db.collection("sUsers").document(auth.currentUser!!.uid)
            docRef.get()
                .addOnSuccessListener { document ->
                    if (document != null) {
                        Log.d("HomeActivity", "DocumentSnapshot data: ${document.data}")
                        username.text = document.getString("Full Name")

                    } else {
                        Log.d("HomeActivity", "No such document")
                    }
                }
                .addOnFailureListener { exception ->
                    Log.d("HomeActivity", "get failed with ", exception)
                }


            username.text = fullname
            Log.d("HomeActivity", "Current Data: " + fullname)
        }

        internal fun setProfileimage(profileImage: String) {
            val userProfileImage = pView.findViewById<CircleImageView>(R.id.profileUserImage)
            Picasso.get().load(profileImage).into(userProfileImage)
        }

    }

Model Class

package Models

    class Users(
    var fullname: String= "",
    var profileImage: String= "",
    var uid: String? = "",
    var haircut: Boolean? = null,
    var waxing: Boolean? = null,
    var nails: Boolean? = null,
    var profileRatingBar: Float? = 1.0f
)

My onStart and onStop


   override fun onStart() {
        super.onStart()
        adapter!!.startListening()
    }

    override fun onStop() {
        super.onStop()

        if (adapter != null) {
            adapter!!.stopListening()
        }
    }

1 Answers1

2

This is how I would write your RecyclerView. Key points:

  • Don't make a 2nd FireStore query inside the ViewHolder
  • Your Firestore schema must exactly match your model
  • Use lifecycle owner instead of onStart/onStop
  • Firebase UI doesn't capture the uid; so do this manually (see apply)
  • ViewHolder must "hold" the views as fields (to avoid calling find every time)
  • Model represents 1 object, so I name it "User" not "Users"
  • Set layoutManager in XML to reduce boilerplate in onCreate

Layout XML

<androidx.recyclerview.widget.RecyclerView
    ...
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    tools:itemCount="5"
    tools:listitem="@layout/display_users_profile"
    ... />

Activity onCreate

val query = FirebaseFirestore.getInstance()
    .collection("sUsers") // Why not "users" ?
    .orderBy("fullname", Query.Direction.DESCENDING)
    .limit(10)

val options = FirestoreRecyclerOptions.Builder<User>()
    .setLifeCycleOwner(this)
    .setQuery(query) { it.toObject(User::class.java)!!.apply { uid = it.id } }
    .build()

all_users_nearby_list.adapter = UserFirestoreRecyclerAdapter(options)

Adapter

internal class UserFirestoreRecyclerAdapter(options: FirestoreRecyclerOptions<User>) : 
    FirestoreRecyclerAdapter<User, UserViewHolder>(options) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 
        LayoutInflater.from(parent.context)
            .inflate(R.layout.display_users_profile, parent, false)
            .let { UserViewHolder(it) }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int, model: Users) = 
        holder.bind(model)

}

ViewHolder

internal class UserViewHolder(itemView: View) : 
    RecyclerView.ViewHolder(itemView) {

    // Hold view refs
    private val usernameTextView: TextView = itemView.userNameTextView
    private val profileUserImage: ImageView = itemView.profileUserImage

    internal fun bind(model: User) {
        model.apply {
            usernameTextView.text = fullname
            Picasso.get().load(profileImage).into(profileUserImage)
        }
    }
}

Model

// Set sensible defaults here (or null if no sensible default)
data class User(
    var uid: String = "",
    var fullname: String= "",
    var profileImage: String= "",
    var haircut: Boolean = false,
    var waxing: Boolean = false,
    var nails: Boolean = false,
    var profileRatingBar: Float? = null
)
charles-allen
  • 3,891
  • 2
  • 23
  • 35
  • I am getting a compilation error on `itemView.userNameTextView` and `itemView.userProfileImage` says Overload resolution ambiguity. Also if my Booleans have values already in the Firestore will it change depending on what value is given? – Timothy Hanmann Oct 29 '19 at 00:59
  • Make sure your synthetic import is for `xxx.yyy.view.*` (and that you don't have a 2nd synthetic import). If you have data in Firestore, the default is ignored; the defaults in your model class are only used if the data is absent. – charles-allen Oct 29 '19 at 01:11
  • Import should be something like this: `import kotlinx.android.synthetic.main.display_users_profile.view.*` (the view at the end is important!) – charles-allen Oct 29 '19 at 01:14
  • I updated my question and those are what I have imported. I added the one you showed me but it says unused – Timothy Hanmann Oct 29 '19 at 01:30
  • Can you share your activity & item layouts please? – charles-allen Oct 29 '19 at 01:34
  • I just spotted that you inverted the variable naming for your image view... your val is `userProfileImage` but the id is `profileUserImage`. Synthetic vals match the ids in the layout. I have fixed my ViewHolder code to use profileUserImage :) – charles-allen Oct 29 '19 at 01:38
  • Ignore the change in the userProfileTextView. I was testing something that maybe I had used the same id earlier with another layout – Timothy Hanmann Oct 29 '19 at 01:43
  • I updated the ViewHolder code; please check :) You could try deleting all synthetic imports and re-importing with `Alt+Enter`, being careful to select the `xxx.view.*` import for your RV item layout. If that doesn't resolve the issue, you might have 2 views that share the same id; you have 3 options: (a) move Adapter/ViewHolder to their own file (also good for readability & reusability); (b) rename view ids to be unique across all layouts; or (c) just use findViewById to find the view explicitly. – charles-allen Oct 29 '19 at 01:46
  • I think I've missed the original intent of your question... you said you have an exception due to a missing getter/setter? Please share your Firestore schema (this sounds like your Firestore keys don't match your model fields). – charles-allen Oct 29 '19 at 01:47
  • Okay I added an image of my Firestore, the view id I am using is unique and has no other uses so don't know why it still says that. I did findViewById but no luck. I am hoping that with these changes I would not have that issue anymore but can't run the emulator yet with that overload ambiguity error – Timothy Hanmann Oct 29 '19 at 02:06
  • You did `val usernameTextView = itemView.findViewById(R.id.usernameTextView)` ? – charles-allen Oct 29 '19 at 02:08
  • @TimothyHanmann - As suspected!! Your Firestore keys don't match your model. Your model fields are nicely named, so I suggest you update your Firestore keys. They must be *exactly* these: `fullname`, `profileImage`, `waxing`, etc. (not "Full Name", etc) – charles-allen Oct 29 '19 at 02:09
  • Okay I made them all lowercase and created them exactly like my model fields. Then for the itemView that is what I did and still giving me the same error. Maybe I should put it into a separate file and try that – Timothy Hanmann Oct 29 '19 at 02:18
  • I definitely encourage splitting out the Adapter & ViewHolder. It gets a bunch of boilerplate code out of your way. You will never look at the Adapter again!! Perhaps you can paste the specific error message you're getting (here in the comments)? Maybe I will notice something. – charles-allen Oct 29 '19 at 02:20
  • C:\Users\TJM\AndroidStudioProjects\Stylez2U\app\src\main\java\com\ia\stylez2u\HomeActivity.kt: (117, 40): Overload resolution ambiguity: private final val itemView: View defined in com.ia.stylez2u.HomeActivity.UserViewHolder @NonNull public final val itemView: View defined in com.ia.stylez2u.HomeActivity.UserViewHolder – Timothy Hanmann Oct 29 '19 at 02:21
  • Stupid me! It shouldn't be private val; it's already declared in the parent class. I have fixed it in my ViewHolder constructor :) (sorry, I thought the error was on the fields, not the param) – charles-allen Oct 29 '19 at 02:22
  • How would I go about a swift transition? Do I just create it as a new Adapter Class? Sorry I used most of this code from what I had before as Java and kept it all in the same activity – Timothy Hanmann Oct 29 '19 at 02:22
  • If you still want to extract, `right-click on the class` > `refactor` > `move` (or just press `F6`). Android Studio should do all the work :) – charles-allen Oct 29 '19 at 02:24
  • Should I add the ViewHolder as well – Timothy Hanmann Oct 29 '19 at 02:31
  • Most people have 1 file for the Activity and 1 file for Adapter & ViewHolder together (since you rarely use the same ViewHolder with multiple Adapters). I personally put all 3 in separate files (because I rarely need to re-read the Adapter code; so I keep it out of my way). I have had the odd occasion where I reused 1 ViewHolder with 2 different Adapters (1x for local data; 1x for Firestore data). – charles-allen Oct 29 '19 at 02:45
  • 1
    Omg! Thank you so much, finally working perfectly. I had to fix a few syntax errors after changing the way the data was displayed in my Firestore keys. I upvoted your answer – Timothy Hanmann Oct 29 '19 at 03:11
  • Hi again, I am having an issue with my boolean values not being retrieved. I used the same values as my model class but still not working – Timothy Hanmann Nov 03 '19 at 19:32
  • I have to admit, I don't think I've even stored a Boolean in Firestore. After doing a little reading, I think perhaps the issue is that Kotlin might be wrapping your Boolean properties in a slightly unique way: `waxing` => `isWaxing` & `setWaxing`. Maybe this answer helps? https://stackoverflow.com/questions/52284861/firebase-firestore-toobject-fails-on-boolean-property-mapping – charles-allen Nov 03 '19 at 23:13
  • BTW, it might be more scalable to store services as an Array and use `services.contains("someService")` (in Kotlin) or `whereArrayContains("services", "someService")` (for queries) – charles-allen Nov 04 '19 at 00:17
  • So I should not use a Hashmap and switch to an ArrayMap? – Timothy Hanmann Nov 04 '19 at 01:38
  • Also, the way I am trying to use my boolean is depending on its value will change an ImageView to a different Image. Something like available and not available, or Black to Grey – Timothy Hanmann Nov 04 '19 at 01:44
  • No, I mean inside your Map (which represents the entire object), replace all your booleans with a single Array: `val user = mutableMapOf("uid" to uid, "fullName" to name, "services" to listOf("haircut", "waxing"), ...more fields...)` – charles-allen Nov 04 '19 at 04:36
  • 1
    Should I change my `val userRef` from a Document Reference to a Collection reference? I got it to list as an array and it looks better now but still having issues with the Boolean value. Might open a new question for this – Timothy Hanmann Nov 06 '19 at 22:49
  • @Timothy Hanmann - honestly it might be quicker; someone who's used booleans in Firestore can just give you the answer! Try to post a minimal reproducible example though (skip anything irrelevant to your current issue). – charles-allen Nov 07 '19 at 00:43
  • Hello again, sorry to bother but how do I add a setOnClickListener with "Intent" to my bindViewHolder now that it is an internal class? Keeps requiring me to add more parameters but I am confused about it. – Timothy Hanmann Nov 17 '19 at 20:06
  • 1
    The cheat way is `itemView.context.startActivity(Intent(itemView.context, OtherActivity::class.java))`. The more decoupled way would be to pass a handler from Activity to Adapter to ViewHolder. – charles-allen Nov 18 '19 at 01:07
  • Well I want to be able to click the views and open the activity that corresponds to that uid. Last time with Realtime Database I used0 `clickProfileActivity,putExtra("profileKey", profileKey)` . I'm not sure if I need to use `adapterPosition` – Timothy Hanmann Nov 19 '19 at 01:56
  • 1
    We already added the id to the model (see the lambda for options setQuery). Just use `intent.putExtra("EXTRA_ID", model.uid)`. All this intent stuff should be in your VH's `bind` method. – charles-allen Nov 19 '19 at 05:19