I am an iPhone developer and currently porting my app to Android with Kotlin language and this is my first android app so I do not know about the Android studio or Kotlin much so bare my question.
I have an app where I show all the images stored into the firebase and each image has like node. So when the users pressed the like button the user Id will be added to the node and pressing the button again will remove the user Id from the firebase node. I use Groupie RecyclerView for the images to for rows. The only problem is when I click the like button the RecyclerView scrolls to the top which is kind of irritating and not a good user interface. How can I stop the like button to stop scrolling to the top, I believe its due scrolling due to firbase database refreshing the RecyclerView.
Below is the function I have used for fetching the data from the database
private fun fetchFactsFromFirebase(){
val factsDb = FirebaseDatabase.getInstance().getReference("/Facts").orderByChild("/timestamp")
factsDb.addValueEventListener(object : ValueEventListener{
override fun onDataChange(p0: DataSnapshot) {
val adapter = GroupAdapter<ViewHolder>()
p0.children.forEach {
val facts = it.getValue(Facts::class.java)
if (facts != null) {
adapter.add(FactsItems(facts))
}
}
recycleview_facts.adapter = adapter
}
Below is the facts class I have used to bind my database to the Row
class FactsItems(val facts: Facts) : Item<ViewHolder>(){
val factsId : String = ""
val currentUser = FirebaseAuth.getInstance().currentUser?.uid
override fun bind(viewHolder: ViewHolder, position: Int) {
viewHolder.itemView.textview_facts.text = facts.captionText
Picasso.get().load(facts.factsLink).placeholder(R.drawable.progress_image).into(viewHolder.itemView.imageview_facts)
viewHolder.itemView.likeview_facts.text = facts.likes.count().toString()
val likeButon = viewHolder.itemView.like_facts_image_button
if (facts.likes.contains(currentUser!!)){
likeButon.setImageResource(R.drawable.like)
likeButon.isSelected = true
}else{
likeButon.setImageResource(R.drawable.nolike)
likeButon.isSelected = false
}
viewHolder.itemView.imageview_facts
viewHolder.itemView.like_facts_image_button.isSelected
viewHolder.itemView.like_facts_image_button.setOnClickListener {
if (likeButon.isSelected == true){
likeButon.isSelected = false
likeButon.setImageResource(R.drawable.nolike)
addSubtractLikes(false,position, viewHolder)
}else {
likeButon.isSelected = true
likeButon.setImageResource(R.drawable.like)
addSubtractLikes(true, position, viewHolder)
}
}
}
Below is the addSubtract function to add or subtract the likes of the user ignore the postion and view holder I just tried to get the postion in this function but it did not work.
fun addSubtractLikes(addlike: Boolean, position: Int, viewHolder: ViewHolder){
val currentUsers = FirebaseAuth.getInstance().currentUser?.uid
if (addlike){
facts.likes.add(currentUsers!!)
Log.d("Like","User Liked ${currentUsers}")
}else {
facts.likes.remove(currentUsers!!)
Log.d("Like","User DisLiked ${currentUsers}")
}
val likeRef = FirebaseDatabase.getInstance().getReference("/Facts").child(facts.factsId).child("/likes")
likeRef.setValue(facts.likes)
Log.d("Like","liked Users ${facts.likes}")
}
What all I have tried.
When I get the instance of the RecyclerView into the factsItem class it gives me an error nullPointerException
so i cannot use RecyclerView.scrollToPostion or RecyclerView any function
Thank you any help will be appreciated.
Related
sorry for my English
I just started learning android and I'm trying to make a library app that uses Room db, recView and fragments
in the app there are 3 lists, all books, already read and Wishlist - the already read and Wishlist are just books that its property isAlreadyRead is true
in the viewModel i created 3 lists members, one for each list
--Book class--
#Entity(tableName = "books_table")
data class Book(
#PrimaryKey(autoGenerate = true)
val id: Int = 1,
val name: String,
val author: String,
val desc: String,
val imgUrl: String,
#ColumnInfo(defaultValue = "0")
val isAlreadyRead: Boolean = false,
#ColumnInfo(defaultValue = "0")
val isWishlist: Boolean = false
)
--repository--
val allBooks = bookDao.getAllBooks().asLiveData()
val alreadyReadBooks = bookDao.getAlreadyReadBooks().asLiveData()
val wishlistBooks = bookDao.getWishlistBooks().asLiveData()
--viewModel--
val allBooks = repository.allBooks
val alreadyReadBooks = repository.alreadyReadBooks
val wishlistBooks = repository.wishlistBooks
(the repository and viewModel is just the important section of the code, if you need the whole code i will update the question)
the problem mainly occurs when i delete an object, i added the ability to delete using swipe with ItemTouchHelper.SimpleCallback
private val itemTouchHelperCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (viewHolder !is BookRecViewAdapter.BookViewHolder) {
return
}
bookViewModel.removeFromWishlistBooks(viewHolder.bookId)
adapter.notifyDataSetChanged()
}
}
when i delete in the already books, the bookViewModel.alreadyReadBooks observer is being called, but i noticed that also the bookViewModel.allBooks observer is being called, here is the code of the observers
--observer code inside onViewCreated of the fragments--
bookViewModel.allBooks.observe(requireActivity(), Observer { allBooks ->
Log.d("observer:", "show all books observer")
// Update the cached copy of the allBooks in the adapter.
allBooks?.let { adapter.books = it }
if (allBooks.isNotEmpty()) {
binding.listEmptyTextView.visibility = View.INVISIBLE
} else {
binding.listEmptyTextView.visibility = View.VISIBLE
}
})
(the code for the already read fragment and the wishist fragment is the same just that i called bookViewModel.alreadyReadBooks)
it crashes the app because in the observer i also want to update the visibility of a textView which just say that the list if empty (so there will be no white screen with no information), but binding is null because the fragment is in the background (i think that is the reason)
if you need any more info or code say, first time writing in stack overflow
thanks in advance
BTW: if you have any advice for me i will be happy to hear
My guess is that you are using requireActivity() instead of viewLifecycleOwner when you observe the bookViewModel. If I didn't guess right, you should show us the code that you have in your Fragment class.
In my Fragment for my Android app, I'm using Firebase Realtime Database and Moshi to save and load the data I get from my RecyclerView.
These are the functions I use for this task:
private fun saveData() {
val moshi = Moshi.Builder().add(BigDecimalAdapter).add(KotlinJsonAdapterFactory()).build()
val listMyData = Types.newParameterizedType(List::class.java, ItemCard::class.java)
val jsonAdapter: JsonAdapter<ArrayList<ItemCard>> = moshi.adapter(listMyData)
val json = jsonAdapter.toJson(dataList)
userInfo.child("jsonData").setValue(json)
}
private fun loadData(json: String) = lifecycleScope.launch(Dispatchers.IO) {
if (json != "") {
val type: Type = object : TypeToken<List<ItemCard>>() {}.type
val moshi = Moshi.Builder().add(BigDecimalAdapter).add(KotlinJsonAdapterFactory()).build()
val jsonAdapter: JsonAdapter<ArrayList<ItemCard>> = moshi.adapter(type)
dataList = jsonAdapter.fromJson(json)!!
if (dataList == null) {
dataList = arrayListOf<ItemCard>()
}
}
}
private fun buildRecyclerView() {
recyclerView = rootView.findViewById(R.id.main_recycler_view)
recyclerView.setHasFixedSize(true)
recyclerViewLayoutManager = LinearLayoutManager(this#Main.requireContext())
adapter = MainAdapter(dataList, this)
recyclerView.layoutManager = recyclerViewLayoutManager
recyclerView.adapter = adapter
}
In my onViewCreated, I having this for loading the data and building the RecyclerView:
userInfo.addValueEventListener(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
for (postSnapshot in dataSnapshot.children) {
when (postSnapshot.key) {
"jsonData" -> {
loadData(postSnapshot.value.toString())
buildRecyclerView()
}
}
}
}
override fun onCancelled(error: DatabaseError) {}
})
Everything works as I want, however, there's a delay/lag when I go to this specific fragment. There are a total of three fragments in my app. The other two work smoothly with no delay/lag, but when I click on the button or slide the screen to go to this fragment, there's a delay in the change of the UI.
What can I do to make the performance better? Where should I put my addValueEventListener? I only want it to get triggered when the fragment is first created and when the jsonData child gets changed. I believe in my onViewCreated the listener is being triggered multiple times. Is there anything else I can add to my code or modify to make the performance better when saving and loading the RecyclerView data?
when you create a listerner in onViewCreated run then make sure that you remove listener when fragment is not attach.Realtime event listerner return a string that you can easily = to your pojo class like:
ItemCard message = messageSnapshot.getValue(ItemCard.class);
this way is to saving manual converting the list effort.Last important thing that if recyclerview is initialize then don't initialize when data change only notifyDataSetChange.when you adding data in the list then make sure that the
list.clear();
otherwise you data is duplicate because on addValueEventListener return the whole data.
I have a recyclerview where when I swipe left I open a bottom sheet and select one value and then I need to update my room DB table and also disable swipe.
But currently im not able to do both things at the same time, only the disable swipe is working not able to update recyclerview item
swipeController = SwipeController(object:SwipeControllerActions() {
override fun onRightClicked(position:Int) {
val bottomSheetFragment = BottomModalFragment()
bottomSheetFragment.show(parentFragmentManager, bottomSheetFragment.tag)
var homeTaskModel: HomeTaskModel = homeAdapter.getWordAtPosition(position)!!
TASK_STATUS.observe(viewLifecycleOwner, Observer {
homeTaskModel.task_status = it //passing value
homeViewModel.updateSwipeType(1,0) //disable swipe
homeAdapter.notifyItemChanged(position)
})
}
Below is my onbindviewholder code
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val tasks = taskslist[position]
var task__status = tasks.task_status
holder.Taskbinding.taskdata = taskslist[position]
holder.Taskbinding.txtTaskname.text = tasks.taskname
holder.Taskbinding.txtTaskStatus.text = task__status
//hide unhide navigation imageview
if (task__status.equals("Travel To Site"))
holder.Taskbinding.imgNavigation.visibility = View.VISIBLE
else
holder.Taskbinding.imgNavigation.visibility = View.GONE
}
getItemViewType() is called before onBindViewHolder where I am updating my views
override fun getItemViewType(position: Int): Int {
return taskslist.get(position).swipe_type
}
How to acheieve both the scenarios???
create refreshList in adapter class the refresh the list
fun refreshList(list:arrayOf()){
tasklist = list
}
call this function from mainActivity where you want to notify the adapter
homeAdapter.refreshList(pass your updated list here)
thats it!!
I am working on an android app and using Kotlin for the first time. The program does the following:
1) Login
2) Write Data to Database (Firebase Realtime Database)
3) Read Data from Database (RecycleView + Adapter with ViewHolder)
4) Users: Normal, Admin, Special
"Normal" users can input data and look at database objects using RecycleView + ViewHolder; "Admin" can overwrite a specific identifier in objects in the database by clicking a button (which is invisible for other users); "Special" users can only see those objects with that specific identifier in them using RecycleView + ViewHolder.
The problem is: specific RecycleView for "Special" users doesn't work. The Activity just crashes. The Adapter for RecycleView works: data is submitted to the database by "normal" users and data is displayed correctly in TextView + ViewHolder, even after updating through Admin user.
Here is my code (### represent hashes that are hardcoded for testing purposes):
class SchadenAdapter : RecyclerView.Adapter<SchadenViewHolder>() {
[...]
class SchadenViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(schaden: Schaden) {
val userId: String = FirebaseAuth.getInstance().currentUser!!.uid
itemView.textViewSchadenort.text = schaden.schadenort
itemView.textViewSchadenart.text = schaden.schadenart
itemView.textViewSchadendatum.text = schaden.datum
itemView.textViewSchadenstatus.text = schaden.status
if (userId == "###") {
itemView.buttonTextViewAllocate.setOnClickListener() {
(R.layout.item_schaden)
// show message box after button click
AlertDialog.Builder(itemView.context)
.setMessage("Schaden zugeteilt.")
.create()
.show()
// get uuid: update "allocate" + "status"
val rootRef = FirebaseDatabase.getInstance().reference
val uuidRef = schaden.uuid
rootRef.child("schäden")
.child(uuidRef)
.child("allocated")
.setValue("###")
rootRef.child("schäden")
.child(uuidRef)
.child("status")
.setValue("###")
}
} else {
itemView.buttonTextViewAllocate.setVisibility(View.GONE)
}
}
}
At first I tried to do the same for "Special" users: I created a button in the layout.xml and just used the same structure in the Adapter - the only difference is that they can only change "status" by clicking a button. But everytime I tried to do so, the app crashed.
For the sake of completeness, here is the code for the Activity that uses the Adapter:
class SchadenListeActivity : AppCompatActivity() {
private lateinit var databaseReference: DatabaseReference
private lateinit var schadenAdapter: SchadenAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_schaden_liste)
databaseReference = FirebaseDatabase.getInstance().reference
schadenAdapter = SchadenAdapter()
recyclerViewSchadenliste.layoutManager = LinearLayoutManager(this)
recyclerViewSchadenliste.adapter = schadenAdapter
val itemDecor = DividerItemDecoration(this, VERTICAL)
recyclerViewSchadenliste.addItemDecoration(itemDecor)
databaseReference.child("schäden")
.addValueEventListener(object : ValueEventListener {
override fun onCancelled(databaseError: DatabaseError) {
Toast.makeText(this#SchadenListeActivity,
"Database error: $databaseError.message", Toast.LENGTH_SHORT).show()
}
override fun onDataChange(dataSnapshot: DataSnapshot) {
val schadenListe = dataSnapshot.children.mapNotNull { it.getValue<Schaden>(Schaden::class.java) }
schadenAdapter.setSchaden(schadenListe)
}
})
}
}
Help is greatly appreciated.
I am not exactly sure of the app crash, what I can observe seeing your code is your are trying to show alert dialog using itemView.context which is not possible. You can show a dialog using activity context only. Try to show the dialog using activity context and check if its still crashing.
Please provide the stacktrace of your current app crash.
I am building an app where user is required to fill some data in order to post something, so a fragment consists of EditText, radio buttons and Spinner along with RecyclerView which dynamically renders a number of child layout containing TextView and EditText.
So when user select category from Spinner, some properties which are related to that category are displayed in RecyclerView and user can optionally fill some of them.
I have tried to implement this functionality using callback and TextWatcher but I don't get the values I want.
CallBack
interface PropertiesCallback {
fun addProp(position: Int, title: String, value: String)
}
Adapter
class PropertiesAdapter(private val propertiesCallback: PropertiesCallback)
: RecyclerView.Adapter<PropertiesAdapter.ViewHolder>() {
private var list = listOf<CategoriesAndSubcategoriesQuery.Property>()
fun setData(listOfProps: List<CategoriesAndSubcategoriesQuery.Property>) {
this.list = listOfProps
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.z_property_input, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int = list.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(list[position], position)
}
inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
private val label: TextView = view.findViewById(R.id.label)
private val input: EditText = view.findViewById(R.id.input)
fun bind(prop: CategoriesAndSubcategoriesQuery.Property, position: Int) {
label.text = prop.title()
prop.hint()?.let { input.hint = prop.hint() }
input.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
propertiesCallback.addProp(position, prop.title(), input.text.toString())
}
})
}
}
}
In Fragment
private var propertiesList = mutableListOf<CategoriesAndSubcategoriesQuery.Property>()
private var propertiesInputList = mutableListOf<ProductPropertiesInput>()
private fun setUpSubcategorySpinner() {
subcategoriesAdapter = ArrayAdapter(
this#AddProductFragment.context!!,
android.R.layout.simple_spinner_item,
subcategoriesList
)
//Subcategories
subcategoriesAdapter.setDropDownViewResource(android.R.layout.simple_dropdown_item_1line)
subcategory_spinner.adapter = subcategoriesAdapter
subcategory_spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
subcategoryId = subcategoriesList[position].id()
//Adding properties
subcategoriesList[position].properties()?.let {
//Clear previous properties data of another subcategory.
propertiesInputList.clear()
propertiesList.clear()
propertiesList.addAll(it)
propertiesAdapter.setData(propertiesList)
propertiesAdapter.notifyDataSetChanged()
}
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
}
overide
override fun addProp(position: Int, title: String, value: String) {
val prop = ProductPropertiesInput
.builder()
.title(title)
.value(value)
.build()
propertiesInputList.add(prop)
//Log.d(TAG, "prop: ${prop.title()} : ${prop.value()}")
}
submit fun
private fun submitProduct() {
//Initializing properties.
val properties: Any
//The keys needed in final list.
val propertyKeys = propertiesList.map { it.title() }
//Removing objects which keys are not needed.
propertiesInputList.removeAll { it.title() !in propertyKeys }
Log.d(TAG, "propertiesInputList: $propertiesInputList")
//Removing duplicate and assign result in properties var.
properties = propertiesInputList
.distinctBy { it.title() }
Log.d(TAG, "properties: $properties")
for (prop in properties) {
Log.d(TAG, "properties , title: ${prop.title()}, value: ${prop.value()} ")
}
}
Above codes is intended to work as. When user types a value in one of the EditText in RecyclerView the value will be taken to fragment and added to an object which takes title and value and then added to propertiesInputList.
Problem 1: propertiesInputList will have so many duplicates objects with the same title and I thought the best solution was using distinctBy.
Problem 2: When user fills a number of EditText which are related to let's say category1 and changes his mind and select another category from Spinner. The previous values which are not part of new chosen category remain in propertiesInputList list. So I thought the best solution was to clear propertiesInputList and using removeAll with the titles related to category to filter unwanted objects.
But now I get only the first letter user types. If user types shoes I get s. So it seems distinctBy returns the first object but I want to get exactly last word user typed and if the user typed and erased everything I want blank.
Is there a better solution to handle this? Like looping recyclerView only when user press submit instead of TextWatcher? Or which part should I fix to make this work?
I don't completely understand what you are trying to achieve here. EditTexts inside a RecyclerView is generally not a good idea for following reasons.
When the recyclerView is scrolled, you would want to preserve the
text added by the user for that particular field/item and show it
correctly when the user scrolls back.
When you add a TextWatcher to an EditText, you also need to remove it when the view is recycled or the view holder is bound again. Otherwise, you will end up with multiple listeners and things will go wrong.
For the other question that you have,
But now I get only the first letter user types. If user types shoes I get s
That's by design. TextWatcher would emit event every time a character is entered. So you would get s, sh, sho, shoe, shoes. So you can not take an action on this data because the user is still adding something to that field.
So,
You don't know when the user has stopped adding the text to the EditText (or whether user is done). You could use something like debounce but that is complicated. You should give a button to the user. Take the value when the user taps the button.
I am assuming you have multiple edittexts in the RecyclerView. So you would need to store the values for each edittext because the recyclerview will re-use the views and you'll lose the data. You could do that in your adapter's onViewRecycled callback. Keep a map of id -> string where you store this data and retrieve when the view holder is bound.
You could also use a TextWatcher but you would have detach it before attaching a new one or in onViewRecycled.
Update:
If I had something like this, I would use a ScrollView with a vertical LinearLayout (for simplicity) and add EditText based on the requirements. If you want to add TextWatcher, you'd need some kind of stable id.
class EditTextContainer : LinearLayout {
private val views = mutableListOf<EditText>()
private val textWatchers = hashMapOf<Int, TextWatcher>()
... constructor and bunch of stuff
fun updateViews(items: List<Item>, textCallback: (id, text) -> Unit) {
// Remove text watchers
views.forEach { view ->
view.removeTextWatcher(textWatchers[view.id])
}
// More views than required
while (views.size > items.size) {
val view = views.removeAt(views.size-1)
removeView(view)
}
// Less views than required
while (view.size < items.size) {
val view = createView()
view.id = View.generateViewId()
addView(view, createParams()) // Add this view to the container
views.add(view)
}
// Update the views
items.forEachIndexed { index, item ->
val editText = views[item]
// Update your edittext.
addTextWatcher(editText, item.id, textCallback)
}
}
private fun createView(): EditText {
// Create new view using inflater or just constructor and return
}
private fun createParams(): LayoutParams {
// Create layout params for the new view
}
private fun addTextWatcher(view, itemId, textCallback) {
val watcher = create text watcher where it invokes textCallback with itemId
view.addTextWatcher(watcher)
textWatchers[view.id] = watcher
}
}
Your inputs are less to identify the issue. I guess you are making some data collection application with the list of edit text.
There is a an issue when you were using the edit text in recycler list.
When you scroll down the bottom edit text in the recycler view will be filled with already filled edit text value, even though you user is not filled.
As a work around You can create some sparse array any data structure which will best suitable for you, that can map you position and value
like
mPropertyValue[] = new String [LIST_SIZE]. , assuming that position of ur list item matches with index of array.
Try updating the index with the value of text watcher
mPropertyValue[POSITION] = YOUR_EDIT_TEXT_VALUE
When you want to initialize your edit text use the value by mPropertyValue[POSITION]
You can always make sure that your edit text will be having the right value by this .
i face like this problem in my java code and that was the solution
for (int i = 0; i < list.size(); i++) {
(put her the getter and setter class) mylist = list.get(i);
//use the getter class to get values and save them or do what ever you want
}