When trying to get JSON data from an API and show it on the RecyclerView I'm getting following error:
I already use RecyclerView sometimes and I never had this problem.
MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(activity_main)
overwriteOnPostInteractionListener()
setupObservers()
}
private fun setupObservers(){
mServiceRequest.searchPostsFromAPI().observe(this, Observer { posts ->
if (posts != null){
loadRecyclerView()
mPostList = posts.toMutableList()
}
})
}
private fun loadRecyclerView() {
recyclerView.adapter = PostListAdapter(mPostList, mOnPostListInteractionListener)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(true)
}
Adapter:
class PostListAdapter(private val postList: List<PostModel>,
private val onPostListInteractionListener: OnPostListInteractionListener):
RecyclerView.Adapter<PostViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
val inflate = LayoutInflater.from(parent.context)
val view = inflate.inflate(R.layout.posts , parent, false)
return PostViewHolder(view, parent.context, onPostListInteractionListener)
}
override fun getItemCount(): Int {
return postList.count()
}
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
holder.bindTask(postList[position])
}}
ViewHolder:
class PostViewHolder(itemView: View, private val context: Context,
private val onPostListInteractionListener: OnPostListInteractionListener)
: RecyclerView.ViewHolder(itemView) {
private val postTitle = itemView.findViewById<TextView>(R.id.titleTextViewMain)
private val postBody = itemView.findViewById<EditText>(R.id.bodyEditText)
fun bindTask(post: PostModel){
postTitle.text = post.title
postBody.setText(post.body)
postTitle.setOnClickListener {
onPostListInteractionListener.onListClick(post.id)
}
}}
I searched a lot how to solve this error but I can't.
You build your RecyclerView adapter before feeding the data to the list, so you need to call mPostList = posts.toMutableList() before instantiating your adapter.
So, change setupObservers() with:
private fun setupObservers(){
mServiceRequest.searchPostsFromAPI().observe(this, Observer { posts ->
if (posts != null){
mPostList = posts.toMutableList()
loadRecyclerView()
}
})
}
Also set your adapter after, you completely build the RecyckerView. So change the order of loadRecyclerView() as below.
private fun loadRecyclerView() {
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(true)
recyclerView.adapter = PostListAdapter(mPostList, mOnPostListInteractionListener)
}
Setting RecyclerView adapter before layout can cause issues.
Side note:
You instantiate your adapter every time your list is changed, and that's not the right way, and it's better to create your stuff (including the adapter) only once in a lifecycle, and then make setters each time you want to change them instead of instantiating them over and over again.
So, you can instantiate your adapter only once in onCreate() method and create a method in your adapter that takes the posts setPosts(private val postList: List<PostModel>) and set them internally .. whenever you need to change adapter list, just call setPosts
Related
I want to refresh my Recycler View, i receive my data by viewModel and pass it for my adapter
so i don’t know how to clear this data and call it again
MainActivity:
class MainActivity : AppCompatActivity() {
private val viewModel: ContatoViewModel = ContatoViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main);
configuraObserver()
viewModel.search()
}
private fun configuraObserver() {
viewModel.contato.observe(this, { data ->
Log.i("API", "Data received")
contato_recyclerview.apply {
layoutManager = LinearLayoutManager(this.context, LinearLayoutManager.VERTICAL, false)
adapter = ContatoAdapter(this.context, data.conteudoResposta)
}
})
}
My Adapter:
class ContatoAdapter(private val context: Context?, private val contatos : List<Contato>) : RecyclerView.Adapter<RecyclerView.ViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.list_item_contato,parent, false)
return ContatoViewHolder(view)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder as ContatoViewHolder
val contato = contatos.elementAt(position)
holder.bindView(contato)
}
override fun getItemCount(): Int {
return contatos.size
}
if you just want to refresh your data(which you already received) in the recycler view you just need to call notifyDataSetChanged() from your Adapter.
SwipeRefreshLayout is needed when you want to implement pull to refresh, which means you want to trigger the initiation of API call when someone pulls down the screen and then after receiving the data you will pass it to Adapter and notifyDataSetChanged()
For implementing pull to refresh you can follow this Google Doc
BACKGROUND
I have a UI that shows a list of users' fullnames with a like/dislike button for each item. I am using a ListAdapter that under the hood uses DiffUtil and AsyncListDiffer APIs. The list of users is received as a LiveData from a Room database and it's ordered by "isLiked".
PROBLEM
Whenever the like button is tapped, Room as I am using a LiveData will re-submit the new data to the adapter. The problem is that as the list is ordered by "isLiked", the liked user will change its position and the RecyclerView will always sroll to the new position.
I don't want to see the new position of the updated item. So, how can I disable the auto scroll behavior?
WHAT I TRIED
MainActivity.kt
..
val userAdapter = UsersAdapter(this)
val ll = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerView.apply {
layoutManager = ll
adapter = userAdapter
itemAnimator = null
setHasFixedSize(true)
}
viewModel.users.observe(this, {
// This will force the recycler view to scroll back to the previous position
// But it's more of a workaround than a clean solution.
val pos = ll.findFirstVisibleItemPosition()
userAdapter.submitList(it) {
recyclerView.scrollToPosition(pos)
}
})
..
UsersAdapter.kt
class UsersAdapter(
private val clickListener: UserClickListener
) : ListAdapter<UserEntity, UsersAdapter.UserViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val userEntity = getItem(position)
holder.bind(userEntity, clickListener)
}
class UserViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val textView: TextView = view.findViewById(R.id.fullName)
private val fav: ImageButton = view.findViewById(R.id.fav)
fun bind(user: UserEntity, clickListener: UserClickListener) {
textView.text = user.fullName
val favResId = if (user.favorite) R.drawable.like else R.drawable.dislike
fav.setImageResource(favResId)
fav.setOnClickListener {
val newFav = !user.favorite
val newFavResId = if (newFav) R.drawable.like else R.drawable.dislike
fav.setImageResource(newFavResId)
clickListener.onUserClicked(user, newFav)
}
}
}
interface UserClickListener {
fun onUserClicked(user: UserEntity, isFavorite: Boolean)
}
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<UserEntity>() {
override fun areItemsTheSame(
oldUser: UserEntity,
newUser: UserEntity
) = oldUser.id == newUser.id
override fun areContentsTheSame(
oldUser: UserEntity,
newUser: UserEntity
) = oldUser.fullName == newUser.fullName && oldUser.favorite == newUser.favorite
}
}
}
I tried using a regular RecyclerView adapter and DiffUtil with detect moves set to false.
I added the AsyncListDiffer as well.
I tried the ListAdapter, and even tried the paging library and used the PagedListAdapter.
DiffUtil's callback changes the auto scrolling, but i couldn't get the desired behavior.
Any help is greatly appreciated!
READ FIRST:
Apologies, it seems I have played myself. I was using RecyclerView in my xml earlier, but switched it over for CardStackView (it still uses the exact same RecyclerView adapter). If I switch back to RecyclerView, the original code below works - the scroll position is saved and restored automatically on configuration change.
I'm using a MVVM viewmodel class which successfully retains list data for a RecyclerView after a configuration change. However, the previous RecyclerView position is not restored. Is this expected behaviour? What would be a good way to solve this?
I saw a blog post on medium briefly mentioning you can preserve scroll position by setting the adapter data before setting said adapter on the RecyclerView.
From what I understand, after a configuration change the livedata that was being observed earlier gets a callback. That callback is where I set my adapter data. But it seems this callback happens after the onCreate() function finishes by which point my RecyclerView adapter is already set.
class MainActivity : AppCompatActivity() {
private val adapter = MovieAdapter()
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Create or retrieve viewmodel and observe data needed for recyclerview
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
viewModel.movies.observe(this, {
adapter.items = it
})
binding.recyclerview.adapter = adapter
// If viewmodel has no data for recyclerview, retrieve it
if (viewModel.movies.value == null) viewModel.retrieveMovies()
}
}
class MovieAdapter :
RecyclerView.Adapter<MovieAdapter.MovieViewHolder>() {
var items: List<Movie> by Delegates.observable(emptyList()) { _, _, _ ->
notifyDataSetChanged()
}
class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val binding = ItemMovieCardBinding.bind(itemView)
fun bind(item: Movie) {
with(binding) {
imagePoster.load(item.posterUrl)
textRating.text = item.rating.toString()
textDate.text = item.date
textOverview.text = item.overview
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_movie_card, parent, false)
return MovieViewHolder(view)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount() = items.size
}
class MainViewModel : ViewModel() {
private val _movies = MutableLiveData<List<Movie>>()
val movies: LiveData<List<Movie>> get() = _movies
fun retrieveMovies() {
viewModelScope.launch {
val client = ApiClient.create()
val result: Movies = withContext(Dispatchers.IO) { client.getPopularMovies() }
_movies.value = result.movies
}
}
}
Set adapter only after its items are available.
viewModel.movies.observe(this, {
adapter.items = it
binding.recyclerview.adapter = adapter
})
I want to databind both the activity and the recyclerview.
But I get this error for the recycler view.
E/RecyclerView: No adapter attached; skipping layout
Removing the code for activity data binding, the recyclerview works.
Activity.kt
class MainActivity : AppCompatActivity(), IActivity {
private lateinit var mIMainPresenter: IPresenter
private lateinit var mMainAdapter: MainAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mIMainPresenter = MainPresenter(this)
mIMainPresenter.getList()
}
/**
* setup UI widgets
*/
private fun setupList() {
val mLayoutManager = LinearLayoutManager(this)
mMainAdapter = MainAdapter(mIMainPresenter)
recyclerList.layoutManager = mLayoutManager
recyclerList.adapter = mMainAdapter
recyclerList.addItemDecoration(DividerItemDecoration(this, VERTICAL))
refresh_layout.setOnRefreshListener {
fetch(null)
refresh_layout.isRefreshing = false
}
}
/**
* fetches list from
*/
override fun fetch(view: View?) {
mIMainPresenter.getList()
}
/**
* sets the list items once data is fetched from network/database
*/
override fun setEvents(result: List<Events>) {
setupList()
mMainAdapter.setList(result)
mMainAdapter.notifyDataSetChanged()
}
override fun setPrompts(result: List<Prompts>) {
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
this, R.layout.activity_main)
binding.prompt = result[0]
}
}
Adapter.kt
class MainAdapter(private val mIClick: IClick) : RecyclerView.Adapter<MainAdapter.AutoViewHolder>() {
private var events: List<Events> = listOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AutoViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ListMainBinding>(inflater, R.layout.list_main, parent, false)
return AutoViewHolder(binding)
}
override fun onBindViewHolder(holder: AutoViewHolder, position: Int) {
val event = events.get(position)
val binding = holder.listMainBinding;
binding?.events = event
binding?.iClick = mIClick
binding?.executePendingBindings()
}
override fun getItemCount(): Int {
return events.size
}
fun setList(result: List<Events>) {
events = result
}
inner class AutoViewHolder : RecyclerView.ViewHolder {
var listMainBinding: ListMainBinding? = null
constructor(binding: ListMainBinding?) : super(binding?.root) {
listMainBinding = binding
}
}
}
If setPrompts is called after setEvents, I'm pretty sure it will have created a new RecyclerView and setupList won't be called after that new recycler view is created. That means the new recycler won't have an adapter.
What you will want to do instead is do the setContentView stuff inside of onCreate, keep a reference to the Binding, and then set prompt on that binding when you get the data from the db or network. You will probably also want to do similar with setEvents, and move the initial list setup into onCreate, and just change out the data when that comes in.
I am new on kotlin android. I have created the adapter for recyclerview. But I am not able to perform a click event for each recyclerview item. I need the explanation with the reference code.
Kindly help me to do this.
Thanks in advance.
Here is my code for your reference.
class CustomAdapter(val readerList: ReaderResponse, mainActivity:
MainActivity,val btnlistener: BtnClickListener) :
RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
companion object {
var mClickListener: BtnClickListener? = null
}
override fun onCreateViewHolder(viewgroup: ViewGroup, index: Int): ViewHolder
{
val view=LayoutInflater.from(viewgroup?.context).inflate(R.layout.reader_list,viewgroup,false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return readerList.results.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
mClickListener = btnlistener
val item = readerList
val reader:ReaderData = readerList.results[position]
/*p0?.imageview?.text=reader.readerIcon*/
holder?.reader_status?.text=reader.readerStatus
holder?.ward_name?.text=reader.wardName
holder?.reader_id?.text=reader.readerID
holder?.reader_name?.text=reader.readerName
holder?.reader_location?.text=reader.readerLocation
if (reader.readerStatus.toLowerCase().equals("yes")){
holder.reader_name.setTextColor(Color.parseColor("#24a314"))
}else if (reader.readerStatus.toLowerCase().equals("no")){
holder.reader_name.setTextColor(Color.parseColor("#f4312d"))
holder.warning.setVisibility(View.VISIBLE)
}
}
class ViewHolder(itemView: View) :RecyclerView.ViewHolder(itemView) {
val imageview = itemView.findViewById(R.id.imageview) as Button
val reader_name = itemView.findViewById(R.id.reader_name) as TextView
val reader_location = itemView.findViewById(R.id.floor_no) as TextView
val ward_name = itemView.findViewById(R.id.ward_name) as TextView
val reader_id = itemView.findViewById(R.id.reader_id) as TextView
val reader_status = itemView.findViewById(R.id.reader_status) as TextView
val warning=itemView.findViewById(R.id.warning) as Button
}
open interface BtnClickListener {
fun onBtnClick(position: Int)
}
}
You could use the following approach. This is taken from this blog by Antonio Leiva
Assuming your data class is ReaderData
class CustomAdapter(val readers: List, val listener: (ReaderData) -> Unit) {
/* Other methods */
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
/*...*/
holder.imageview.setOnClickListener {
listener(readers[position])
}
}
}
Now in your Activity or Fragment
recyclerview.adapter = CustomAdapter(readersList) { readerData ->
Log.i(TAG, "${readerData.readerID} clicked")
}
The idea is you pass a lambda which will be executed when your desired item is clicked.
You just need to implement BtnClickListener in the corresponding Activity in which this adapter is initialized. Once you have implemented the BtnClickListener it would override the function onBtnClick in the activity.
The only thing you need to do in the adapter is to initialize the onClickListener on the element you need and in that method just call imageview.setOnClickListener { mClickListener?.onBtnClick(position) }. It would send the position back in activity and you can perform your specific task there. For example I have implemented the ClickListener in one Activity and printed the log there it works fine. Below is the demo code for it.
class Main2Activity : AppCompatActivity(), CustomAdapter.BtnClickListener {
override fun onBtnClick(position: Int) {
Log.d("Position", position.toString())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main2)
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayout.VERTICAL, false)
val readerResponseList = ArrayList<YourModelClassName>()
val adapter = CustomAdapter(readerResponseList,this,this)
recyclerView.adapter = adapter
}
Hope it Helps.