0

I am simply trying to setup the ClickListener for changing the user name. Google's tutorials emphasize fragments, which for the moment feels like overkill.

The app crashes when I navigate to the ManageUsers activity. Based on what I've seen in other examples and the Android documentation, I thought I had the View Binding set up properly.

UserListAdapter.kt

package com.neillbarrett.debitsandcredits

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.neillbarrett.debitsandcredits.database.UsersTable
import com.neillbarrett.debitsandcredits.databinding.ActivityManageUsersBinding

class UserListAdapter(private val userSelect: (UsersTable?) -> Unit) :
    ListAdapter<UsersTable, UserListAdapter.UserViewHolder>(UsersComparator()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserListAdapter.UserViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.activity_manage_users, parent, false)
        return UserViewHolder.create(parent)
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        val current = getItem(position)
        holder.bind(current, userSelect)
    }

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

        private val userView: TextView = itemView.findViewById(R.id.tv_UserName)
//        var text: String? = null

        fun bind(usersTable: UsersTable?, userSelect: (UsersTable?) -> Unit, text: String = usersTable?.userName.toString()) {

            userView.text = text

            itemView.setOnClickListener { View.OnClickListener {
/*                if (View.) { }*/

                val nameSelected = userSelect(usersTable)
                //userSelect(usersTable)
                //need to assign the result of the clicklistener to the editText
                //binding.etEditName.setText(R.layout.activity_list_of_users.toString())
            }}
        }

        companion object {
            fun create(parent: ViewGroup) : UserViewHolder {
                val view: View = LayoutInflater.from(parent.context)
                    .inflate(R.layout.activity_manage_users, parent, false)
                return UserViewHolder(view)
            }
        }
    }

    class UsersComparator : DiffUtil.ItemCallback<UsersTable>() {
        override fun areItemsTheSame(oldItem: UsersTable, newItem: UsersTable): Boolean {
            return oldItem.userName == newItem.userName

        }

        override fun areContentsTheSame(oldItem: UsersTable, newItem: UsersTable): Boolean {
            return oldItem == newItem
        }
    }
}

ManageUsers.kt

package com.neillbarrett.debitsandcredits

import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.TextUtils
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.activity.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.neillbarrett.debitsandcredits.database.CreditsAndDebitsApp
import com.neillbarrett.debitsandcredits.database.UsersTable
import com.neillbarrett.debitsandcredits.databinding.ActivityManageUsersBinding

class ManageUsers : AppCompatActivity() {

    lateinit var binding: ActivityManageUsersBinding
    lateinit var recyclerView: RecyclerView
    lateinit var editTextAddUser: EditText
    lateinit var editTextChangeUser: EditText
    lateinit var newUser: String
    var userSelect: ((UsersTable?) -> Unit) = {}
    var position: Long = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityManageUsersBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
        //setContentView(R.layout.activity_manage_users)

        val userViewModel: UserViewModel by viewModels {
            UserViewModelFactory((application as CreditsAndDebitsApp).repository)
        }

        recyclerView = findViewById(R.id.rec_view_userList)
        editTextAddUser = findViewById(R.id.et_AddUser)
        editTextChangeUser = findViewById(R.id.et_Edit_Name)

        val adapter = UserListAdapter(userSelect)
        binding.recViewUserList.adapter = adapter
        binding.recViewUserList.layoutManager = LinearLayoutManager(this)
        //recyclerView.adapter = adapter
        //recyclerView.layoutManager = LinearLayoutManager(this)

        userViewModel.allUsers.observe(this, Observer() {user ->
            user?.let { adapter.submitList(it) }
        })

        val btnAddUser = findViewById<Button>(R.id.btn_AddUser)
        binding.btnAddUser.setOnClickListener {
//        btnAddUser.setOnClickListener {
            if (TextUtils.isEmpty(editTextAddUser.text)) {
                Toast.makeText(this, "User name cannot be empty", Toast.LENGTH_SHORT).show()
            } else {
                newUser = editTextAddUser.text.toString()
//                Log.i("Add user button", "Username put into newUser")
                userViewModel.insertUser(UsersTable(0, newUser))
//                Toast.makeText(this, "Username added to table", Toast.LENGTH_SHORT).show()
//                Log.i("Add user button", "Username added to table")
            }
        }

        val btnChangeUser = findViewById<Button>(R.id.btn_ChangeUserName)
        binding.btnChangeUserName.setOnClickListener {
//        btnChangeUser.setOnClickListener {
            Toast.makeText(this, "Selected position is ${recyclerView.getChildAdapterPosition(it)}", Toast.LENGTH_SHORT).show()
/*            if (recyclerView.getChildAdapterPosition(it) == -1) {
                Toast.makeText(this, "Select a name.", Toast.LENGTH_SHORT).show()
            } else {
                if (editTextChangeUser.text.toString() == recyclerView.adapter.toString()) {
                    Toast.makeText(this, "Name has not been changed.", Toast.LENGTH_SHORT).show()
                } else {
                    Toast.makeText(this, "Name would have been changed.", Toast.LENGTH_SHORT).show()
                    val rvItemRecId: Long
                    rvItemRecId = adapter.getItemId(position.toInt())
                    userViewModel.updateUser(UsersTable(rvItemRecId.toInt(), adapter.toString()))
                }
            }*/
        }
    }
}

UserViewModel.kt

package com.neillbarrett.debitsandcredits

import androidx.lifecycle.*
import com.neillbarrett.debitsandcredits.database.UsersTable
import kotlinx.coroutines.launch
import java.lang.IllegalArgumentException

class UserViewModel(private val repository: UserRepository) : ViewModel() {

    // Using LiveData and caching what allWords returns has several benefits:
    // - We can put an observer on the data (instead of polling for changes) and only update the
    //   the UI when the data actually changes.
    // - Repository is completely separated from the UI through the ViewModel.
    val allUsers: LiveData<List<UsersTable>> = repository.allUsers.asLiveData()

    /**
     * Launching a new coroutine to insert the data in a non-blocking way
     */

    fun insertUser(user: UsersTable) = viewModelScope.launch {
        repository.insertUser(user)
    //repository.insertUser(usersTable = List<UsersTable>())
    //repository.insertUser(UsersTable(0, userName = user.userName))
    }

    fun updateUser(user: UsersTable) = viewModelScope.launch {
        repository.updateUser(user)
    }

    fun deleteUser(user: UsersTable) = viewModelScope.launch {
        repository.deleteUser(user)
    }
}

class UserViewModelFactory(private val repository: UserRepository) : ViewModelProvider.Factory{
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return UserViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

The LogCat shows this:

2022-11-25 11:43:59.427 8217-8217/com.neillbarrett.debitsandcredits E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.neillbarrett.debitsandcredits, PID: 8217
    java.lang.NullPointerException: itemView.findViewById(R.id.tv_UserName) must not be null
        at com.neillbarrett.debitsandcredits.UserListAdapter$UserViewHolder.<init>(UserListAdapter.kt:30)
        at com.neillbarrett.debitsandcredits.UserListAdapter$UserViewHolder$Companion.create(UserListAdapter.kt:51)
        at com.neillbarrett.debitsandcredits.UserListAdapter.onCreateViewHolder(UserListAdapter.kt:21)
        at com.neillbarrett.debitsandcredits.UserListAdapter.onCreateViewHolder(UserListAdapter.kt:15)
        at androidx.recyclerview.widget.RecyclerView$Adapter.createViewHolder(RecyclerView.java:7078)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6235)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6118)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6114)
        at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
        at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1627)
        at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
        at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:665)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4134)
        at androidx.recyclerview.widget.RecyclerView.onMeasure(RecyclerView.java:3540)
        at android.view.View.measure(View.java:26411)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7845)
        at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
        at android.widget.LinearLayout.measureVertical(LinearLayout.java:842)
        at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
        at android.view.View.measure(View.java:26411)
        at androidx.constraintlayout.widget.ConstraintLayout$Measurer.measure(ConstraintLayout.java:811)
        at androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.measure(BasicMeasure.java:466)
        at androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.measureChildren(BasicMeasure.java:134)
        at androidx.constraintlayout.core.widgets.analyzer.BasicMeasure.solverMeasure(BasicMeasure.java:278)
        at androidx.constraintlayout.core.widgets.ConstraintWidgetContainer.measure(ConstraintWidgetContainer.java:120)
        at androidx.constraintlayout.widget.ConstraintLayout.resolveSystem(ConstraintLayout.java:1594)
        at androidx.constraintlayout.widget.ConstraintLayout.onMeasure(ConstraintLayout.java:1708)
        at android.view.View.measure(View.java:26411)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7845)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
        at androidx.appcompat.widget.ContentFrameLayout.onMeasure(ContentFrameLayout.java:145)
        at android.view.View.measure(View.java:26411)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7845)
        at androidx.appcompat.widget.ActionBarOverlayLayout.onMeasure(ActionBarOverlayLayout.java:496)
        at android.view.View.measure(View.java:26411)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7845)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
        at android.view.View.measure(View.java:26411)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7845)
        at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1552)
        at android.widget.LinearLayout.measureVertical(LinearLayout.java:842)
        at android.widget.LinearLayout.onMeasure(LinearLayout.java:721)
        at android.view.View.measure(View.java:26411)
        at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:7845)
        at android.widget.FrameLayout.onMeasure(FrameLayout.java:194)
        at com.android.internal.policy.DecorView.onMeasure(DecorView.java:1050)
        at android.view.View.measure(View.java:26411)
        at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:3635)

It's clear I'm missing something, but I have no idea what.

N.Barrett
  • 61
  • 7
  • You should not be using findViewById if you’re using view binding. It’s crashing because you’re using findViewById with an ID that isn’t in the current layout. The whole point of view binding is to avoid using findViewById so you can avoid this kind of crash. – Tenfour04 Nov 28 '22 at 04:48
  • You haven’t used view binding at all in your adapter. That’s where your crash is happening as you can see from the stack trace. It’s ok to use findViewById as long as you know which view IDs are in the view you’re searching. – Tenfour04 Nov 28 '22 at 04:53
  • It'll be a couple days before I can rectify that. I saw binding.root in someone else's example. I didn't realize that was the key. More to the point, Google's documentation _**fails to state that**_, which strikes me as a _substantial_ oversight, at least for those of us who are trying to learn. I am going to mention that the next time I am presented with a developer survey. – N.Barrett Nov 28 '22 at 09:54
  • I've changed it to `class UserViewHolder(private val binding: ActivityManageUsersBinding) : RecyclerView.ViewHolder(binding.root)` and `return UserViewHolder(view)` to `return UserViewHolder(ActivityManageUsersBinding.bind(view))` but I still see nothing in the RecyclerView. It's not clear how to bind the text view. – N.Barrett Nov 29 '22 at 15:16
  • So at least the crash is fixed now. Now you can use debugging and breakpoints to see if your code is being called when you expect. I would start by putting a breakpoint in your live data observer to see if your repository is publishing lists. Side note, you have the functionality of `areItemsTheSame` and `areContentsTheSame` swapped from the proper meaning. That will cause problems for you when you change the list data. – Tenfour04 Nov 29 '22 at 15:22
  • In a previous, more basic commit, the list displayed just fine and changed appropriately when I added entries. I hope debugging and breakpoints can pinpoint what changes broke the RecyclerView. Also, thank you for pointing out the Comparator issue. – N.Barrett Nov 29 '22 at 17:57
  • I put the debugger on `userViewModel.allUsers.observe(this, Observer() {user -> user?.let { adapter.submitList(it) }})`. I started it and see what look like functions, parameters, and variables. I don't know how to interpret that – N.Barrett Nov 30 '22 at 19:20
  • You can look up tutorials on how debugging works. – Tenfour04 Nov 30 '22 at 20:55
  • I added the line needed for debugging in `build.gradle`. I put Breakpoints for `userViewModel.allUsers.observe(this, Observer() {user -> user?.let { adapter.submitList(it) }})` in ManageUsers.kt and alternatively `binding.btnUpdateUsers.setOnClickListener {startActivity(Intent(this@MainActivity, ManageUsers::class.java))}`. – N.Barrett Dec 03 '22 at 19:16
  • When I look at the variables in the Debug window in either case, I see a bunch of things that confuse me. They all speak of looking at `ERROR_ELEMENT '$delegate(1,14) in /fragment.kt`, when I never built any of these screens with fragments. – N.Barrett Dec 03 '22 at 19:16
  • A few steps into debugging, I start seeing, `view = Cannot find local variable 'view'`, `userViewModel$delegate = Cannot find local variable 'userViewModel$delegate'` and `((UserListAdapter)adapter).userSelect = Cannot find local variable 'adapter'`. In later steps I also see `thread = Cannot find local variable 'thread'`. Does this suggest that the app is glitching so badly it can't even load from the repository? – N.Barrett Dec 03 '22 at 19:17
  • That’s not a thing. It sounds like something about your debugging is misconfigured, but I’ve never had it not work for me so I don’t know how to troubleshoot it. – Tenfour04 Dec 03 '22 at 21:23
  • ​I hate to say this, but this post has been deeply unhelpful for solving this problem. The debugger presents the same type of problem I always run into when I try to solve a new problem in Android Studio + Kotlin. It's messy and unintuitive to sort out simply how to make it work correctly. I need better and more specific information on what to look for and how to deal with it. I estimate it would take me at least 6 months to figure out what's going on. – N.Barrett Dec 08 '22 at 18:35
  • As for simply setting up the ClickListener, that's probably another 6 months' to a year's worth of work, maybe longer. It's actually very challenging trying to find an example of how to do what I want to do. It is not your fault that Google's tutorials and documentation are limited, badly written, or both. It's also not clear if I've made a bad design choice in how I wanted to setup this screen. – N.Barrett Dec 08 '22 at 18:36
  • I originally suggested setting a breakpoint so the code execution will pause in the observer if it's getting called at all. So you don't necessarily need to have variable values inspection working to be able to determine that. But I do agree about Google documentation being a problem--it's mainly because they can't make up their mind about best practices. They are constantly changing their recommended best practices and deprecating previous best practices. – Tenfour04 Dec 08 '22 at 19:20
  • I will have to experiment with code execution to see how that works. One thing that confuses me thoroughly is, do I need to use an `ArrayList` of my UserTable as one of the arguments for the UserListAdapter? I can't tell if that's the source of some problems (or if it would make some techniques easier). – N.Barrett Dec 08 '22 at 20:07
  • Adding the breakpoint shows that the observer is indeed being called, at least from I can tell. – N.Barrett Dec 09 '22 at 20:02
  • A tooltip has popped up in several places and says, "Source code does not match the bytecode". Any idea what that means? Also, I've seen "Sources for 'Android API 33, extension level 3 Platform'" in a few spots. It offers to download that and to refresh if already downloaded. I assume it's prodding me? – N.Barrett Dec 09 '22 at 20:04
  • Should I put a breakpoint on `adapter.submitList(it)` to see if that's being called? – N.Barrett Dec 09 '22 at 20:06
  • Sure, why not? It takes just a minute to check it. Might as well put break points in `onBindView` and elsewhere up and down the chain. But if you can't get debugging to show you variable values, you could also try logging instead. By the way, I just noticed you have `areItemsTheSame` and `areContentsTheSame` conceptually swapped. You will eventually need to fix that. – Tenfour04 Dec 09 '22 at 20:26
  • Sounds like your source code is somehow out of date. Might as well update that and refresh. That could have been the problem with your debugging failing to show variable values, but I'm not positive. – Tenfour04 Dec 09 '22 at 20:27
  • The last time you or I posted here, the website stated we should move to chat. – N.Barrett Dec 10 '22 at 19:42
  • I am on the AndroidDev Discord server. I told them I was having problems with the debugger. Since I use suspend functions for my database (in line with Google's tutorials), that broke the debugger: https://youtrack.jetbrains.com/issue/KT-48678. Until they fix that, I'll have to use LogCat. – N.Barrett Dec 16 '22 at 18:58
  • I guess I never really noticed this problem. Must be a weird coincidence that I haven't needed to debug variable values inside a lambda before or something. – Tenfour04 Dec 16 '22 at 19:32
  • When I ran the LogCat, I discovered `No adapter attached; skipping layout` after the pieces of UserListAdapter are called (outside of `UsersComparator`). I found posts [here](https://stackoverflow.com/questions/29141729/recyclerview-no-adapter-attached-skipping-layout), [here](https://www.edureka.co/community/175129/recyclerview-no-adapter-attached-skipping-layout), and [here](https://makecodesimpleandeasy.blogspot.com/p/erecyclerview-no-adapter-attached.html) about that, I just don't have a clue about how to implement them. – N.Barrett Dec 22 '22 at 20:27
  • You can post a question about your specific case with code to get help with that. That warning message appears whenever the UI is drawn before an adapter has been added to the RecyclerView. It doesn't necessarily mean there's a problem. Maybe you just want to show nothing until the data becomes available. But it's a bit of a code smell that maybe you are creating a new adapter each time new data becomes available. – Tenfour04 Dec 30 '22 at 02:31
  • What's a "code smell"? – N.Barrett Dec 30 '22 at 19:14
  • A sign that something’s probably (but not necessarily) designed incorrectly or has a fragile (bug-prone) design. If an adapter is used correctly, it is very unlikely you would encounter this warning, but the warning itself does not necessarily mean you’ve done something wrong for sure. – Tenfour04 Dec 30 '22 at 20:54
  • At this point, the major issue is figuring out why Recyclerview shows a copy of its activity instead of a list from the table. – N.Barrett Dec 30 '22 at 20:58
  • I do, however, appreciate the guidance. ☺️ – N.Barrett Dec 31 '22 at 00:31

0 Answers0