In my SettingActivity I'm loading some data to be shown in ListPreference from Room, once the items are selected all works correctly, the value is saved to `SharedPreferences and the summary is shown correctly, but once I return to SettingsActivity the summary value is reset to null.
Here is what is happening:
My code is pretty simple, onViewCreated() I start observing LiveData to be shown in ListPreference and then I set the values for entries and entryValues
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.shops.observe(viewLifecycleOwner) {
setShopsPreferences(it)
}
}
private fun setShopsPreferences(shops: List<Shop>) {
val shopsPreference = preferenceManager.findPreference<ListPreference>("defaultShop")
if (shops.isEmpty()) {
shopsPreference?.isEnabled = false
return
} else {
shopsPreference?.isEnabled = true
}
val entries: ArrayList<String> = ArrayList()
val entryValues: ArrayList<String> = ArrayList()
shops.forEach {
entries.add(it.description)
entryValues.add(it.id)
}
shopsPreference?.entryValues = entryValues.toArray(arrayOfNulls<CharSequence>(entryValues.size))
shopsPreference?.entries = entries.toArray(arrayOfNulls<CharSequence>(entries.size))
}
ViewModel:
#HiltViewModel
class ShopsViewModel #Inject constructor(repository: ShopsRepository) : ViewModel() {
private val _shops = MutableLiveData<List<Shop>>()
val shops: LiveData<List<Shop>> = _shops
init {
repository.getAllShops().observeForever {
_shops.value = it
}
}
}
Repository:
fun getAllShops(): LiveData<List<Shop>> {
return shops.select()
}
DAO:
#Dao
interface ShopsDAO {
#Query("SELECT * FROM shops")
fun select(): LiveData<List<Shop>>
}
You are populating list entries dynamically after the preference hierarchy is already created by inflating the xml. But during that time there was no entry, hence the value was null. The data was then retrieved asynchronously but the change will not be reflected. So you have to set the summary manually.
Another approach I'm not sure about is to call recreate on the activity after populating the data inside the observer listener.
To resolve the issue with dynamic data in a ListPreference I've made some changes to my code, first of call in onCreatePreferences() I've added a preference listener to my ListPreference like this:
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
val shopsPreference = preferenceManager.findPreference<ListPreference>("defaultShop")
shopsPreference?.setOnPreferenceChangeListener { preference, newValue ->
val description = viewModel.shops.value?.find { it.id == newValue }
preference.summary = description?.description
true
}
}
So in that way on each new selection, I'm looking for the matching value for that preference in my ViewModel and getting the description for it which then is set as the summary.
While to set the summary every time the list has been changed or the SettingsActivity is opened I've added the following code to my setShopsPreferences() function:
private fun setShopsPreferences(shops: List<Shop>?) {
if (shops == null) {
return
}
val shopsPreference = preferenceManager.findPreference<ListPreference>("defaultShop")
// The preference is enabled only if there are shows inside the list
shopsPreference?.isEnabled = shops.isNotEmpty()
// Getting the defaultShop preference value, which will be used to find the actual shop description
val defaultShop = sharedPreferences.getString("defaultShop", "0")
defaultShop?.let { shopId ->
// Setting the summary based on defaultShop saved preference
shopsPreference?.summary = shops.find { it.id == shopId }?.description
}
val entries: ArrayList<String> = ArrayList()
val entryValues: ArrayList<String> = ArrayList()
shops.forEach {
entries.add(it.description)
entryValues.add(it.id)
}
shopsPreference?.entryValues =
entryValues.toArray(arrayOfNulls<CharSequence>(entryValues.size))
shopsPreference?.entries = entries.toArray(arrayOfNulls<CharSequence>(entries.size))
}
In my app i'm saving all data into realm now here when data is loaded into recycler view in my recycler view item i'm having a button there on click of button i'm setting the status field bool of realm database to true and thereafter on dataset changed i'm trying to set an item button background if boolean inside database is true but here i'm having no luck whenever my recyclerview is refreshed in not getting my desired output maybe something is wrong with my data fetching query any help will be appreciated.
Here is my recycler view on bindview holder code:
//this is the code where i'm facing problem i want the else want to execute but that's not happening
val `object` = realm!!.where(Devicesdatabase::class.java).equalTo("status", false).findFirst()
if (`object` != null) {
holder.btn.setBackgroundResource(R.drawable.list_button)
holder.btn.text = "Stranger"
}
else
{
holder.btn.setBackgroundResource(R.drawable.list_button_grey)
holder.btn.setText("known")
}
Here's what i'm doing inside on button click:
holder.btn.setOnClickListener {
holder.btn.setBackgroundResource(R.drawable.list_button_grey)
holder.btn.setText("known")
val `object` = realm!!.where(Devicesdatabase::class.java)
.equalTo("devicename", host.hostname)
.findFirst()
if (`object` != null) {
realm!!.executeTransaction {
var item = realm!!.createObject(Devicesdatabase::class.java)
item.status = true
notifyDataSetChanged()
}
}
}
This is my realm model class:
#RealmClass
open class Devicesdatabase : RealmModel {
var wifi:String?=null
var devicename: String? = null
var macAddress: String? = null
var status:Boolean = false
var type:String?=null
}
How does one properly send data to child adapter in a fragment?
I'm basically trying to implement an Instagram like comments-section, e.g. a bunch of comments that can each have more comments (replies).
To do that, I use one main recyclerView + main adapter, which instances are retained in my fragment, and within the main adapter I bind the children comments (recyclerView + adapter).
Adding comments to the main adapter is easy since the object is always available in the fragment, so I just call mainAdapter.addComments(newComments):
MainAdapter
fun addComments(newComments: List<Comment>){
comments.addAll( 0, newComments) //loading comments or previous comments go to the beginning
notifyItemRangeInserted(0, newComments.size)
}
But how to call addComments of one particular nested-rV? I read I should not save the adapter instances and only use positions.
I'm trying to do that in my Fragment as follows:
val item = rVComments.findViewHolderForItemId(mAdapter.itemId)!!.itemView
val adapt = item.rVReplies.adapter as ChildCommentsAdapter
adapt.addComment(it.data.comment)
But that doesn't work very well: since we have only RecyclerViews, that particular ViewHolder is often already recycled if the user scrolled after posting or fetching items, which leads to a NullPointerException.
Hence the initial question: how does one properly interact with nested recyclerviews and their adapter? If the answer is via Interface, please provide an example as I've tried it without success since I shouldn't save adapter objects.
You can achieve that using a single multi-view type adapter by placing the comments
as part of the parent item, with that, you add the child items below the parent item and call notifyItemRangeInserted.
That way you don't have to deal with most of the recycling issues.
When you want to update a comment you just update the comment inside the parent item and call notifyItemChanged.
If you want I created a library that can generate that code for you in compile time.
It supports the exact case you wanted and much more.
Using #Gil Goldzweig's suggestion, here is what I did: in case of an Instagram like comments' system with replies, I did use a nested recyclerView system. It just makes it easier to add and remove items. However, as for the question How does one properly send data to child adapter in a fragment? You don't. It gets super messy. From my fragment, I sent the data to my mainAdapter, which in turn sent the data to the relevant childAdapter. The key to make it smooth is using notifyItemRangeInserted when adding a comment to the mainAdapter and then notifyItemChanged when adding replies to a comment. The second event will allow sending data to the child adapter using the payload. Here's the code in case other people are interested:
Fragment
class CommentsFragment : androidx.fragment.app.Fragment(), Injectable,
SendCommentButton.OnSendClickListener, CommentsAdapter.Listener {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by lazy {
ViewModelProviders.of(requireActivity(), viewModelFactory).get(CommentsViewModel::class.java)
}
private val searchViewModel by lazy {
ViewModelProviders.of(requireActivity(), viewModelFactory).get(SearchViewModel::class.java)
}
private val mAdapter = CommentsAdapter(this)
private var contentid: Int = 0 //store the contentid to process further posts or requests for more comments
private var isLoadingMoreComments: Boolean = false //used to check if we should fetch more comments
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_comments, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
//hide the action bar
activity?.findViewById<BottomNavigationView>(R.id.bottomNavView)?.visibility = View.GONE
contentid = arguments!!.getInt("contentid") //argument is mandatory, since comment is only available on content
ivBackArrow.setOnClickListener{ activity!!.onBackPressed() }
viewModel.initComments(contentid) //fetch comments
val layoutManager = LinearLayoutManager(this.context)
layoutManager.stackFromEnd = true
rVComments.layoutManager = layoutManager
mAdapter.setHasStableIds(true)
rVComments.adapter = mAdapter
setupObserver() //observe initial comments response
setupSendCommentButton()
post_comment_text.setSearchViewModel(searchViewModel)
setupScrollListener(layoutManager) //scroll listener to load more comments
iVCancelReplyTo.setOnClickListener{
//reset ReplyTo function
resetReplyLayout()
}
}
private fun loadMoreComments(){
viewModel.fetchMoreComments(contentid, mAdapter.itemCount)
setupObserver()
}
/*
1.check if not already loading
2.check scroll position 0
3.check total visible items != total recycle items
4.check itemcount to make sure we can still make request
*/
private fun setupScrollListener(layoutManager: LinearLayoutManager){
rVComments.addOnScrollListener(object: RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = rVComments.childCount
val totalItemCount = layoutManager.itemCount
val pos = layoutManager.findFirstCompletelyVisibleItemPosition()
if(!isLoadingMoreComments && pos==0 && visibleItemCount!=totalItemCount && mAdapter.itemCount%10==0){
//fetch more comments
isLoadingMoreComments = true
loadMoreComments()
}
}
})
}
private fun setupSendCommentButton() {
btnSendComment.setOnSendClickListener(this)
}
override fun onSendClickListener(v: View?) {
if(isInputValid(post_comment_text.text.toString())) {
val isReply = mAdapter.commentid!=null
viewModel.postComment(post_comment_text.text.toString(), mAdapter.commentid?: contentid, isReply) //get reply ID, otherwise contentID
observePost()
post_comment_text.setText("")
btnSendComment.setCurrentState(SendCommentButton.STATE_DONE)
}
}
override fun postCommentAsReply(username: String) {
//main adapter method to post a reply
val replyText = "${getString(R.string.replyingTo)} $username"
tVReplyTo.text = replyText
layoutReplyTo.visibility=View.VISIBLE
post_comment_text.requestFocus()
}
override fun fetchReplies(commentid: Int, commentsCount: Int) {
//main adapter method to fetch replies
if(!isLoadingMoreComments){ //load one series at a time
isLoadingMoreComments = true
viewModel.fetchReplies(commentid, commentsCount)
viewModel.replies.observe(this, Observer<Resource<List<Comment>>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
//showProgressBar(false)
isLoadingMoreComments = false
}
Resource.Status.SUCCESS -> {
isLoadingMoreComments = false
mAdapter.addReplies(mAdapter.replyCommentPosition!!, it.data)
rVComments.scrollToPosition(mAdapter.replyCommentPosition!!)
}
}
})
}
}
private fun isInputValid(text: String): Boolean = text.isNotEmpty()
private fun observePost(){
viewModel.postComment.observe(this, Observer<Resource<PostCommentResponse>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
//showProgressBar(false)
}
Resource.Status.SUCCESS -> {
if(it.data.asReply){
//dispatch comment to child adapter via main adapter
mAdapter.addReply(mAdapter.replyCommentPosition!!, it.data.comment)
rVComments.scrollToPosition(mAdapter.replyCommentPosition!!)
}else{
mAdapter.addComment(it.data.comment)
}
resetReplyLayout()
//showProgressBar(false)
}
}
})
}
private fun setupObserver(){
viewModel.comments.observe(this, Observer<Resource<List<Comment>>> {
if (it?.data != null) when (it.status) {
Resource.Status.LOADING -> {
//showProgressBar(true)
}
Resource.Status.ERROR -> {
isLoadingMoreComments = false
//showProgressBar(false)
}
Resource.Status.SUCCESS -> {
mAdapter.addComments(it.data)
isLoadingMoreComments = false
//showProgressBar(false)
}
}
})
}
private fun resetReplyLayout(){
layoutReplyTo.visibility=View.GONE
mAdapter.replyCommentPosition = null
mAdapter.commentid = null
}
override fun onStop() {
super.onStop()
activity?.findViewById<BottomNavigationView>(R.id.bottomNavView)?.visibility = View.VISIBLE
}
}
MainAdapter
class CommentsAdapter(private val listener: Listener) : RecyclerView.Adapter<CommentsAdapter.ViewHolder>(), ChildCommentsAdapter.ChildListener {
//method from child adapter
override fun postChildReply(replyid: Int, username: String, position: Int) {
commentid = replyid
replyCommentPosition = position
listener.postCommentAsReply(username)
}
interface Listener {
fun postCommentAsReply(username: String)
fun fetchReplies(commentid: Int, commentsCount: Int=0)
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
private var comments = mutableListOf<Comment>()
private var repliesVisibility = mutableListOf<Boolean>() //used to store visibility state for replies
var replyCommentPosition: Int? = null //store the main comment's position
var commentid: Int? = null //used to indicate which comment is replied to
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_comment, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val comment = comments[position]
with(holder.view) {
//reset visibilities (rebinding purpose)
rVReplies.visibility = View.GONE
iVMoreReplies.visibility = View.GONE
tVReplies.visibility = View.GONE
content.loadUserPhoto(comment.avatarThumbnailURL)
text.setCaptionText(comment.username!!, comment.comment)
tvTimestamp.setTimeStamp(comment.timestamp!!)
val child = ChildCommentsAdapter(
//we pass parent commentid and position to child to be able to pass it again on click
this#CommentsAdapter, comments[holder.adapterPosition].id!!, holder.adapterPosition
)
val layoutManager = LinearLayoutManager(this.context)
rVReplies.layoutManager = layoutManager
rVReplies.adapter = child
//initial visibility block when binding the viewHolder
val txtMore = this.resources.getString(R.string.show_more_replies)
if(comment.repliesCount>0) {
tVReplies.visibility = View.VISIBLE
if (repliesVisibility[position]) {
//replies are to be shown directly
rVReplies.visibility = View.VISIBLE
child.addComments(comment.replies!!)
tVReplies.text = resources.getString(R.string.hide_replies)
if (comment.repliesCount > comment.replies!!.size) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}
} else {
//replies all hidden
val txt = txtMore + " (${comment.repliesCount})"
tVReplies.text = txt
}
}
//second visibility block when toggling with the show more/hide textView
tVReplies.setOnClickListener{
//toggle child recyclerView visibility and change textView text
if(holder.view.rVReplies.visibility == View.GONE){
//show stuff
if(comment.replies!!.isEmpty()){
Timber.d(holder.adapterPosition.toString())
//fetch replies if none were fetched yet
replyCommentPosition = holder.adapterPosition
listener.fetchReplies(comments[holder.adapterPosition].id!!)
}else{
//load comments into adapter if not already
if(comment.replies!!.size>child.comments.size){child.addComments(comment.replies!!)}
}
repliesVisibility[position] = true
holder.view.rVReplies.visibility = View.VISIBLE
holder.view.tVReplies.text = holder.view.resources.getString(R.string.hide_replies)
if (comment.repliesCount > comment.replies!!.size && comment.replies!!.isNotEmpty()) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}
}else{
//hide replies and change text
repliesVisibility[position] = false
holder.view.rVReplies.visibility = View.GONE
holder.view.iVMoreReplies.visibility = View.GONE
val txt = txtMore + " (${comment.repliesCount})"
holder.view.tVReplies.text = txt
}
}
tvReply.setOnClickListener{
replyCommentPosition = holder.adapterPosition
commentid = comments[holder.adapterPosition].id!!
listener.postCommentAsReply(comments[holder.adapterPosition].username!!)
}
iVMoreReplies.setOnClickListener{
replyCommentPosition = holder.adapterPosition
listener.fetchReplies(comments[holder.adapterPosition].id!!, layoutManager.itemCount) //pass amount of replies too
}
}
}
#Suppress("UNCHECKED_CAST")
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList<Any>) {
if(payloads.isNotEmpty()){
//add reply to child adapter
with(holder.view){
Timber.d(payloads.toString())
val adapter = rVReplies.adapter as ChildCommentsAdapter
if(payloads[0] is Comment){
adapter.addComment(payloads[0] as Comment)
}else{
//will be of type List<Comment>
adapter.addComments(payloads[0] as List<Comment>)
val comment = comments[position]
if (comment.repliesCount > comment.replies!!.size) {
//show the load more replies arrow if we can fetch more replies
iVMoreReplies.visibility = View.VISIBLE
}else{
iVMoreReplies.visibility = View.GONE
}
}
}
}else{
super.onBindViewHolder(holder,position, payloads) //delegate to normal binding process
}
}
override fun getItemCount(): Int = comments.size
//add multiple replies to child adapter at pos 0
fun addReplies(position: Int, newComments: List<Comment>){
comments[position].replies!!.addAll(0, newComments)
notifyItemChanged(position, newComments)
}
//add a single reply to child adapter at last position
fun addReply(position: Int, newComment: Comment){
comments[position].replies!!.add(newComment)
comments[position].repliesCount += 1 //update replies count in case viewHolder gets rebinded
notifyItemChanged(position, newComment)
}
//add a new comment to main adapter at last position
fun addComment(comment: Comment){
comments.add(comment) //new comment just made goes to the end
repliesVisibility.add(false)
notifyItemInserted(itemCount-1)
}
//add multiple new comments to main adapter at pos 0
fun addComments(newComments: List<Comment>){
comments.addAll( 0, newComments) //loading comments or previous comments go to the beginning
repliesVisibility.addAll(0, List(newComments.size) { false })
notifyItemRangeInserted(0, newComments.size)
}
}
The childAdapter is very basic and has nearly 0 logic.
I am developing a simple app, where people can add products to a basket (in the ProductsFragment) and then can change the quantity of the products in the basket (in the BasketFragment). I am using a RecyclerView with a ListAdapter for both fragments. I am using wrapping a LiveData instance around the Basket so I can observe any changes in the fragment. When I change the quantity the fragment is being notified and when this happens I am using the submitList() method of the ListAdapter to refresh only the changed data in the recyclerview. However, the DiffUtil areContentsTheSame() method is always returning true and so the RecyclerView items are not being updated until I go back to the previous fragment and back to the basket or I scroll the list.
ViewModel
private val _basket = MutableLiveData<Basket>()
val basket: LiveData<Basket>
get() = _basket
fun onIncrementBasketProductQuantityButtonPressed(product: Product) {
val newProduct = product.copy(quantity = product.quantity.plus(1))
val newList= ArrayList(_basket.value!!.basket)
val newBasket = _basket.value!!.copy(basket = newList)
newBasket.updateProductInBasket(newProduct, newProduct.quantity)
_basket.value = newBasket
}
Basket
data class Basket(#Json(name = "basket") val basket: MutableList<Product> = arrayListOf()): Serializable {
fun updateProductInBasket(product: Product, quantity: Int) {
val p = findProduct(product)
if (p != null) {
p.quantity = quantity
p.setSubTotal()
} else {
addNewProductToBasket(product, quantity)
}
}
private fun addNewProductToBasket(product: Product, quantity: Int) {
product.quantity = quantity
product.setSubTotal()
this.basket.add(product)
}
private fun findProduct(product: Product) : Product? {
return this.basket.find { it.name == product.name && it.size == product.size }
}
}
Let's define one variable in the begin, it will be more easier to read:
sqlite3 column KEY_TITLE = A
I have activity in conduction with cursor loader. When there are no rows in column A I want activity to display an emptyView. To do that, I created a method which checks all rows in column A if there is any data in it.
fun hideLayout(){
val isEmpty = emptyViewObserver(applicationContext).isEmpty()
if (isEmpty) {
recyclerView.visibility = View.GONE
emptyView.visibility = View.VISIBLE
} else {
recyclerView.visibility = View.VISIBLE
emptyView.visibility = View.GONE
}
}
class emptyViewObserver with isEmpty method:
class emptyViewObserver(val context: Context){
fun isEmpty():Boolean{
val db = WalletDbHelper(context).readableDatabase
val projection = arrayOf(KEY_TITLE)
val cursor = db.query(
WalletEntry.TABLE_NAME,
projection,
null,
null,
null,
null,
null
)
val names = mutableListOf<String>()
with(cursor){
while (moveToNext()){
val name = getString(cursor.getColumnIndexOrThrow(WalletEntry.KEY_TITLE))
names.add(name)
}
}
db.close()
return names.isEmpty()
}
}
Whenever I perform any CRUD operation in MainActivity class I call hideLayout method to check out if emptyView should be shown or not. It works fine. But I'm facing problem after I perform delete operation from my adapter to call hideLayout() method. For obvious reasons it throws me error when calling it directly from adapter so I though about implementing interface and overriding this in MainActivity. I read some tutorials but I still didn't figure out how to implement this properly.
This is my delete method from adapter:
holder.delete.setOnClickListener { v ->
val popup = PopupMenu(context, v)
val inflater = popup.menuInflater
inflater.inflate(R.menu.delete, popup.menu)
popup.show()
popup.setOnMenuItemClickListener { v ->
when(v.itemId) {
R.id.delete -> {
delete(uri, walletName)
true
}
else -> {
false
}
}
}
}
and delete():
fun delete(uri: String, walletName: String){
val newUri: Uri = Uri.parse(uri)
context.contentResolver.delete(newUri, WalletEntry.KEY_TITLE, arrayOf(walletName))
}
Do you have any ideas how can I write this code?
You need a callback, When you delete some item, check if is the last one and if it is true call that.
Add this interface on your adapter:
interface OnListIsEmpty{
fun onListIsEmpty()
}
And create a object of this like global variable in your adapter too:
private var mOnListIsEmpty: OnListIsEmpty? = null
Add a setter method:
fun setOnListIsEmpty(onListIsEmpty: OnListIsEmpty){
this.mOnListIsEmpty = onListIsEmpty
}
And in your delete method do the validation if is empty if it is true then:
mOnListIsEmpty?.onListIsEmpty()
And not forget set the callback when you initilize the adapter with:
adapter.setOnListIsEmpty(object : OnListIsEmpty{
override fun onListIsEmpty() {
//your action
}
})