Problems with SearchView (items appearing and dissapearing when a button is clicked) - android

I have a list of items on a Recyclerview, and I also have a SearchView to filther those items.
Every item has a favourite button, so when you click, the item adds to favorite table.
The problem is that, when I filter something and I start clicking those buttons, odd things happens: some items dissapear from the filtered list. It doesn't happen always, only sometimes. How can I fix this?
My code:
My class:
class CoasterFragment : Fragment() {
private val myAdapter by lazy { CoasterRecyclerViewAdapter(CoasterListenerImpl(requireContext(), viewModel),requireContext()) }
private lateinit var searchView: SearchView
private var _binding: FragmentCoasterBinding? = null
private val binding get() = _binding!!
private val viewModel: CoastersViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCoasterBinding.inflate(inflater, container, false)
val root: View = binding.root
val recyclerView = binding.recyclerCoaster
recyclerView.adapter = myAdapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
viewModel.coasters().observe(viewLifecycleOwner){myAdapter.setData(it)}
searchView = binding.search
searchView.clearFocus()
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
if(query != null){
searchDatabase(query)
searchView.clearFocus()
}
return true
}
override fun onQueryTextChange(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
})
return root
}
fun searchDatabase(query: String) {
val searchQuery = "%$query%"
viewModel.searchDatabase(searchQuery).observe(viewLifecycleOwner) { myAdapter.setData(it)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Here is my adapter code:
class CoasterRecyclerViewAdapter( val listener: CoasterListener,
val context: Context ) : RecyclerView.Adapter<CoasterRecyclerViewAdapter.ViewHolder>(){
private var coasterList = emptyList<CoasterFavorito>()
class ViewHolder private constructor(val binding: CoasterItemBinding, private val listener: CoasterListener,
private val context: Context): RecyclerView.ViewHolder(binding.root){
companion object{
fun crearViewHolder(parent: ViewGroup, listener: CoasterListener, context: Context):ViewHolder{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CoasterItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding, listener, context )
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder.crearViewHolder(parent, listener, context)
override fun onBindViewHolder(holder: ViewHolder, position: Int){
holder.binding.nombre.text = coasterList[position].coaster.nombre
holder.binding.parque.text = coasterList[position].coaster.parque
holder.binding.ciudad.text = coasterList[position].coaster.ciudad
holder.binding.provincia.text = coasterList[position].coaster.provincia
holder.binding.comunidad.text = coasterList[position].coaster.comunidadAutonoma
Glide
.with(context)
.load(coasterList[position].coaster.imagen)
.centerCrop()
.into(holder.binding.imagen)
holder.binding.check.isChecked = coasterList[position].favorito
holder.binding.check.setOnClickListener{
if (coasterList[position].favorito) {
listener.delFavorito(coasterList[position].coaster.id)
holder.binding.check.isChecked = false
} else {
listener.addFavorito(coasterList[position].coaster.id)
holder.binding.check.isChecked = true
}
}
}
override fun getItemCount(): Int{
return coasterList.size
}
fun setData(coaster: List<CoasterFavorito>){
coasterList = coaster
notifyDataSetChanged()
}
}
interface CoasterListener {
fun addFavorito(id: Long)
fun delFavorito(id: Long)
}
The search Query:
#Query ("SELECT c.*, " + "EXISTS (SELECT * from montarse where usuario_id=:id and coaster_id = c.id) as favorito " + "FROM coasters c " + "WHERE nombre LIKE :searchQuery OR parque LIKE :searchQuery OR ciudad LIKE :searchQuery OR comunidadAutonoma LIKE :searchQuery OR provincia LIKE :searchQuery")
fun searchCoaster(id: Long, searchQuery: String): Flow<List<CoasterFavorito>>
My viewModel:
fun searchDatabase( searchQuery: String): LiveData<List<CoasterFavorito>> {
return coasterDao.searchCoaster( App.getUsuario()!!.id, searchQuery).asLiveData()
}
fun addFavorito( coasterId: Long) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
usuarioCoasterDao.create(UsuarioCoaster(App.getUsuario()!!.id, coasterId, null, null))
}
}
}
fun coasters(): LiveData<List<CoasterFavorito>> {
return coasterDao.findAllFav(App.getUsuario()!!.id).asLiveData()
}
my XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.coaster.HomeCoasterFragment">
<androidx.appcompat.widget.SearchView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="#+id/search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:iconifiedByDefault="false"
app:searchHintIcon="#null"
android:queryHint="Buscar..."
android:focusable="false"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerCoaster"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="5dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="50dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/search"
tools:listitem="#layout/coaster_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
I tried changind the clear focus().
Also I added some if else (for example, if the searchView is Empty, then load the list from the adapter as normal. If it is not empty, use the SearchView code to filter and load the list.

Related

My recyclerview resets when I add or delete an item from a Table (with Room) in Kotlin

First of all, I am Spanish so my english is not good.
I have an app with Kotlin and room, and it has a Recyclerview.
I have 3 tables: coaster, user and favorite.
The user can add coasters to favorite, and this is done succesfully.
The problem that I have is that when the user clicks on the button to add or delete from favorites, the recyclerview resets, it displays again. So it scrolls to the top of the Screen, and also some odd spaces appears after the element.
I also have a function to search, and it happens the same: spaces appears after each element when I am searching.
I have tried everything: notifyItemChanged,
notifyDataSetChanged... it doesnt work! I also tried removing the observer once from the recyclerview...
My main activity:
class CoasterFragment : Fragment() {
lateinit var coasterListener: CoasterListener
lateinit var usuarioCoaster: List\<UsuarioCoaster\>
private lateinit var searchView: SearchView
private lateinit var cAdapter: CoasterRecyclerViewAdapter
private var \_binding: FragmentCoasterBinding? = null
private val binding get() = \_binding!!
private val viewModel: CoastersViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCoasterBinding.inflate(inflater, container, false)
val root: View = binding.root
/* val livedata = viewModel.coasters()
livedata.observe(viewLifecycleOwner,object: Observer <List<CoasterFavorito>> {
override fun onChanged(it: List<CoasterFavorito>) {
createRecyclerView(it)
livedata.removeObserver(this)
}
})*/
viewModel.coasters().observe(viewLifecycleOwner){createRecyclerView(it)}
coasterListener = CoasterListenerImpl(requireContext(), viewModel)
searchView = binding.search
searchView.clearFocus()
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
override fun onQueryTextSubmit(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
override fun onQueryTextChange(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
})
return root
}
fun createRecyclerView(coasters: List<CoasterFavorito>) {
cAdapter =
CoasterRecyclerViewAdapter(
coasters as MutableList<CoasterFavorito>,
coasterListener,
requireContext()
)
val recyclerView = binding.recyclerCoaster
recyclerView.apply {
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
adapter = cAdapter
addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
cAdapter.notifyDataSetChanged()
}
}
fun searchDatabase(query: String) {
val searchQuery = "%$query%"
viewModel.searchDatabase(searchQuery).observe(viewLifecycleOwner) { createRecyclerView(it)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
my adapter:
class CoasterRecyclerViewAdapter(val coasters: List<CoasterFavorito>, val listener: CoasterListener,
val context: Context, ) : RecyclerView.Adapter<CoasterRecyclerViewAdapter.ViewHolder>(){
class ViewHolder private constructor(val binding: CoasterItemBinding, private val listener: CoasterListener,
private val context: Context): RecyclerView.ViewHolder(binding.root){
fun relleno(data: CoasterFavorito){
binding.nombre.text = data.coaster.nombre
binding.parque.text = data.coaster.parque
binding.ciudad.text = data.coaster.ciudad
binding.provincia.text = data.coaster.provincia
binding.comunidad.text = data.coaster.comunidadAutonoma
Glide
.with(context)
.load(data.coaster.imagen)
.centerCrop()
.into(binding.imagen)
binding.check.isChecked = data.favorito
binding.check.setOnClickListener{
if (data.favorito) {
listener.delFavorito(data.coaster.id)
binding.check.isChecked = false
} else {
listener.addFavorito(data.coaster.id)
binding.check.isChecked = true
}
}
}
companion object{
fun crearViewHolder(parent: ViewGroup, listener: CoasterListener, adapter: CoasterRecyclerViewAdapter, context: Context):ViewHolder{
val layoutInflater = LayoutInflater.from(parent.context)
val binding = CoasterItemBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding, listener, context )
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder.crearViewHolder(parent, listener, this, context)
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.relleno(coasters[position])
override fun getItemCount() = coasters.size
}
interface CoasterListener {
fun addFavorito(id: Long)
fun delFavorito(id: Long)
}
I have tried everything: notifyItemChanged,
notifyDataSetChanged... it doesnt work! I also tried removing the observer once from the recyclerview...
Your createRecyclerView function should be invoked only once in a whole lifecycle of the Fragment. You should not create any new RecyclerView.Adapter, or set a LayoutManager to the RecyclerView every time your data set changes.
Therefore the Observer used in viewModel.coasters.observe() should only submit a new List to the existing Adapter and call .notifyDataSetChanged(), or other notifying functions.

Swipe down update from Firebase in RecyclerView

I am trying to make update of my RecyclerViewer with data from Storage for profile picture and Firestore for other data. But I tried many methods and I don't know how to make this.
The goal is to make a RecyclerViewer update each time user goes to fragment and also be able to swipe to refresh the list. Also the layout gets inflated after second 'visit' on the Fragment, the first time it does not get inflated. Is this a problem with a Glide?
And the cache keeps the data in RecyclerViewer even when new user is logged in with completely different data in Firebase.
fragment_item_list.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/swiperefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/itemContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FriendFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list"
android:name="com.msmmhm.hauimetyourmother.FriendFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:layoutManager="LinearLayoutManager"
tools:context=".FriendFragment"
tools:listitem="#layout/fragment_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
FriendFragment.kt
imports ...
class FriendFragment : Fragment() {
private lateinit var binding: FragmentItemListBinding
private lateinit var mSwipeRefreshLayout: SwipeRefreshLayout
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentItemListBinding.inflate(inflater, container, false)
with(binding.list) {
layoutManager = LinearLayoutManager(context)
adapter = MyFriendRecyclerViewAdapter(Friend.ITEMS,container!!.context)
return binding.root
}
}
}
MyFriendRecyclerViewAdapter.kt
imports ...
class MyFriendRecyclerViewAdapter(
private val values: List<FriendItem>,
val context: Context
) : RecyclerView.Adapter<MyFriendRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
FragmentItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = values[position]
var events: CollectionReference = FirebaseFirestore.getInstance().collection("events")
val docRef = events.document(item.id)
Log.i(ContentValues.TAG,"EVENTS ITEM ID: "+item.id)
docRef.get()
.addOnSuccessListener { document ->
if (document.exists())
{
Log.i(ContentValues.TAG,"DOCUMENT EXISTS")
val map = document.data
if (map!!.size != 0)
{
holder.activeIcon.setColorFilter(R.drawable.presence_online)
}
else
{
holder.activeIcon.setImageResource(R.drawable.presence_invisible)
}
}
else
{
holder.activeIcon.setImageResource(R.drawable.presence_invisible)
}
}
holder.friendNickname.text = item.friendName
loadWithGlide2(item.id,holder)
}
fun loadWithGlide2(id: String, holder: ViewHolder) {
val storageReference = Firebase.storage.reference.child("ProfilePictures/${id}.jpg")
Log.d(ContentValues.TAG, "NAME IS: ${storageReference.name}")
Glide.with(context)
.load(storageReference)
.error(R.drawable.defaultprofilepicture)
.into(holder.friendImg)
}
override fun getItemCount(): Int = values.size
inner class ViewHolder(binding: FragmentItemBinding) : RecyclerView.ViewHolder(binding.root) {
val friendImg: ImageView = binding.friendPicture
var friendNickname: TextView = binding.friendNickname
var activeIcon: ImageView = binding.activeIcon
val itemContainer: View = binding.root
override fun toString(): String {
return super.toString() + " '" + friendNickname.text + "'"
}
}
}

Flow is not working with Binding Adapters

For a particular Recycler View Item, If I select the Checkbox (tick it) then I need the text of its corresponding TextView to formatted as Strikethrough.
I am using Binding Adapters, Flow and Live Data.
But after selecting the checkbox, its corresponding TextView is not getting formatted.
But If I navigate to some other fragment and come back to here(FruitFragmnet) then the TextView data is formatted. (i.e. the database gets updated correctly on ticking checkbox but the live data emission is delayed to UI)
Possible Root Cause: My update to Room Database is happening immeialtey, but from database the LiveData is not flown to UI immediately.
I did lot of trial and errors, read multiple similar questions but I was unable to find the missing link and solution to this issue.
Please advice. Following is the code:
BindingAdapter
#BindingAdapter("markAsCompleted")
fun markAsCompleted(textView: TextView, completed: Boolean) {
if (completed) {
textView.paintFlags = textView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
textView.paintFlags = textView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
#BindingAdapter("setItems")
fun setItems(view: RecyclerView, items: List<Fruit>?) {
items?.let {
(view.adapter as SettingAdapter).submitList(items)
}
}
Fruit Fragment with Recycler View
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="settingViewModel"
type="com.example.ui.SettingViewModel" />
</data>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/fruits_list"
setItems="#{settingViewModel.allList}" // This is Binding Adapter
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</layout>
Above Fruit's Fragment Item View
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.widget.CompoundButton" />
<variable
name="fruit"
type="com.example.data.Fruit" />
<variable
name="settingViewModel"
type="com.example.ui.SettingViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
...
<CheckBox
android:id="#+id/fruit_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="#{fruit.completed}"
android:onClick="#{(view) -> settingViewModel.completeFruit(fruit,((CompoundButton)view).isChecked())}"
/>
<TextView
android:id="#+id/fruit_name"
markAsCompleted="#{fruit.completed}" // This is Binding Adapter
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="#{fruit.fruit}" />
....
Fruit Fragment
class FruitFragment : Fragment() {
private lateinit var binding: FragmentFruitBinding
private lateinit var fruitAdapter: FruitAdapter
private val viewModel: SettingViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentFruitBinding.inflate(layoutInflater, container, false).apply {
lifecycleOwner = viewLifecycleOwner
settingViewModel = viewModel
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fruitAdapter = FruitAdapter(viewModel)
binding.fruitslist.apply {
adapter = fruitAdapter
}
}
}
SettingViewModel
class SettingViewModel(application: Application) : AndroidViewModel(application) {
private val app = getApplication<Application>()
private val dao = Database.getDatabase(app.applicationContext).dao
val allList: LiveData<List<Fruit>> = dao.getFruits().asLiveData().distinctUntilChanged()
fun completeFruit(fruit: Fruit, completed: Boolean) {
viewModelScope.launch {
if (completed) {
dao.updateCompleted(fruit.id, completed)
} else {
dao.updateCompleted(fruit.id, completed)
}
}
}
....
}
DAO Class
#Dao
interface DatabaseDao {
#Query("SELECT * FROM fruit_table")
fun getFruits(): Flow<List<Fruit>>
#Query("UPDATE fruit_table SET completed = :completed WHERE id = :id")
suspend fun updateCompleted(id: Int, completed: Boolean)
}
Recycler View Adapter
class FruitAdapter(private val viewModel: SettingViewModel) : ListAdapter<Fruit, ViewHolder>(FruitDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder.from(parent)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item, viewModel)
}
class ViewHolder private constructor(val binding: ContainerFruitBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Fruit, viewModel: SettingViewModel) {
binding.apply {
settingViewModel = viewModel
fruit = item
executePendingBindings()
}
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ContainerFruitBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
}
}
class FruitDiffCallback : DiffUtil.ItemCallback<Fruit>() {
override fun areItemsTheSame(oldItem: Fruit, newItem: Fruit): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Fruit, newItem: Fruit): Boolean {
return oldItem.fruit == newItem.fruit
}
}
Data Class
#Entity(tableName = "fruit_table")
data class Fruit(
#PrimaryKey(autoGenerate = true)
var id: Int = 0,
var fruit: String,
var completed: Boolean = false
)
I guess you need to change the second parameter of setItems function to LiveData in BindingAdapter:
#BindingAdapter("setItems")
fun setItems(view: RecyclerView, data: LiveData<List<Fruit>>) {
data.value?.let {
(view.adapter as SettingAdapter).submitList(it)
}
}

Retrofit into Glide

I am using Retrofit and Glide for a simple Image and textView. In my "Description" fragment I want to display the image from the api into the ImageView just under the textView. My recyclerView images work perfectly, but how would I display a single image from the retrofit/api into this Description class.
I added
val imageView:ImageView = view!!.findViewById(R.id.item_image)
But I am lost after this.
This is the fragment with the textview and where I want to place the ImageView under the text description
class DescriptionFragment : Fragment() {
companion object {
fun newInstance() = DescriptionFragment()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.main_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val textView:TextView = view!!.findViewById(R.id.message)
val imageView:ImageView = view!!.findViewById(R.id.item_image)
// Get the arguments from the caller fragment/activity
val description = arguments?.getString("description")
description?.let {
textView.text = "Description : \n$description"
}
}
}
This is the xml I would like to load the image into
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/description"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.DescriptionFragment">
<ImageView
android:id="#+id/item_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="#+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="41dp"
android:text="#string/full_repo_description"
app:layout_constraintBottom_toTopOf="#+id/message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.503"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" />
<TextView
android:id="#+id/message"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_margin="36dp"
android:text="DescriptionFragment"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
This is the data class where I get the avatar_url from
data class owner(var avatar_url:String);
data class Items(var id: String, var name: String, var full_name: String,var description: String,var owner: owner)
This is my current recycler class which is working correctly
class RecyclerAdapter(private val context: Context) : RecyclerView.Adapter<RecyclerAdapter.MyViewHolder>() {
var itemList: List<Items> = listOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.card_layout, parent, false)
return MyViewHolder(view)
}
override fun getItemCount(): Int {
return itemList.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.repoId.text = itemList[position].id
holder.repoName.text = itemList[position].name
holder.repoFullName.text = itemList[position].full_name
Glide.with(context).load(itemList[position].owner.avatar_url)
.apply(RequestOptions().centerCrop())
.into(holder.repoImage)
holder.itemView.setOnClickListener {
val intent = Intent(context, DescriptionActivity::class.java)
// To pass any data to next activity
intent.putExtra("description", itemList[position].description)
// start your next activity
context.startActivity(intent)
// Toast.makeText(context,itemList.get(position).id,Toast.LENGTH_SHORT).show()
}
}
fun setItemsList(itemList: List<Items>) {
this.itemList = itemList;
notifyDataSetChanged()
}
class MyViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView!!) {
val repoId: TextView = itemView!!.findViewById(R.id.item_id)
val repoName: TextView = itemView!!.findViewById(R.id.item_name)
val repoFullName: TextView = itemView!!.findViewById(R.id.item_fullname)
val repoImage: ImageView = itemView!!.findViewById(R.id.item_image)
}
}
And this is my current mainActivity
class MainActivity : AppCompatActivity() {
lateinit var recyclerView: RecyclerView
lateinit var recyclerAdapter: RecyclerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById(R.id.recyclerview)
recyclerAdapter = RecyclerAdapter(this)
recyclerview.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = recyclerAdapter
val apiInterface = ApiInterface.create().getItems()
apiInterface.enqueue(object : Callback<List<Items>> {
override fun onResponse(call: Call<List<Items>>?, response: Response<List<Items>>?) {
if (response?.body() != null) {
Log.d("myTest", response.body().toString());
recyclerAdapter.setItemsList(response.body()!!)
}
}
override fun onFailure(call: Call<List<Items>>?, t: Throwable?) {
}
})
}
}
And the way I get my url with Retrofit
interface ApiInterface {
#GET("repos")
fun getItems() : Call<List<Items>>
companion object {
var BASE_URL = "https://api.github.com/orgs/square/"
fun create() : ApiInterface {
val retrofit = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(BASE_URL)
.build()
return retrofit.create(ApiInterface::class.java)
}
}
}
I strongly recommend using data binding and add a binding adapter to load your image using Glide
[ UPDATE 2 ]
Consider using fixed value size for Imageview (Width and Height) when you want to show images dynamically (in this case loading from Network)
[ UPDATE ]
Look closely into your RecyclerAdapter class. You have to put the imageUrl in your Intent using this code snippet:
intent.putExtra("imageUrl", itemList[position].owner.avatar_url)
then in your Description{Fragment/Activity} you have to get that url and let the glide do the rest:
val imageUrl = arguments?.getString("imageUrl")
imageUrl?.let { loadImageViaUrl(imageview, it) }
and define loadImageViaUrl as below :
private fun loadImageViaUrl(view: ImageView, url: String) {
val options = RequestOptions()
.placeholder(R.drawable.network_image_placeholder) // use any image as placeholder
Glide.with(view.context)
.setDefaultRequestOptions(options)
.load(url)
.into(view)
}
This is the most basic way to use the Glide library.

RecyclerView is not updated until swiped

I am working on the Movies app where I want to load movies and save as cache in local db. I was following the Udacity's "Building Andoid apps with Kotlin" course "Mars real estate" example. My app is using Live data, MVVM, databinding, Room and Retrofit. Each time new data is loaded, it is saved in db, and is the single source of 'truth'.
This is the main layout:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.moviesapp.presentation.main.MainViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.moviesapp.presentation.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/movies_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="6dp"
android:clipToPadding="false"
android:background="#android:color/black"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:listData="#{viewModel.movies}"
app:spanCount="3"
tools:itemCount="16"
tools:listitem="#layout/grid_view_item"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Fragment:
class MainFragment : DaggerFragment(), SharedPreferences.OnSharedPreferenceChangeListener {
/**
// * Lazily initialize [MainViewModel].
// */
private val viewModel: MainViewModel by lazy {
ViewModelProviders.of(this, providerFactory).get(MainViewModel::class.java)
}
#Inject
lateinit var providerFactory: ViewModelProviderFactory
#Inject
lateinit var sharedPreferences: SharedPreferences
override fun onResume() {
super.onResume()
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
val binding = FragmentMainBinding.inflate(inflater)
binding.lifecycleOwner = this
binding.viewModel = viewModel
binding.moviesGrid.adapter = MainAdapter(MainAdapter.OnClickListener {
viewModel.displayMovieDetails(it)
})
viewModel.navigateToSelectedMovie.observe(this, Observer {
if (null != it) {
this.findNavController().navigate(MainFragmentDirections.actionShowDetail(it.id))
viewModel.displayMovieDetailsComplete()
}
})
setHasOptionsMenu(true)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_settings -> {
this.findNavController().navigate(
MainFragmentDirections.actionShowSettings()
)
true
}
R.id.action_search -> {
this.findNavController().navigate(
MainFragmentDirections.actionMainFragmentToSearchFragment()
)
true
}
R.id.action_back -> {
viewModel.paginateBack()
true
}
R.id.action_forward -> {
viewModel.paginateForward()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
viewModel.onPreferencesChanged()
}
}
View Model:
class MainViewModel #Inject constructor(application: Application, private val getMoviesUseCase: GetMoviesUseCase) :
AndroidViewModel(application) {
private var currentPage = 1
private val totalPages: Int by lazy {
with (PreferenceManager.getDefaultSharedPreferences(application)) {
getInt(TOTAL_PAGES, 0)
}
}
// get movies saved in local db
val movies = getMoviesUseCase.allMovies()
init {
loadMovies()
}
private fun loadMovies() {
if (isInternetAvailable(getApplication()))
viewModelScope.launch {
getMoviesUseCase.refreshMovies(currentPage)
}
}
private val _navigateToSelectedMovie = MutableLiveData<Movie>()
val navigateToSelectedMovie: LiveData<Movie>
get() = _navigateToSelectedMovie
fun displayMovieDetails(movie: Movie?) {
if (movie != null) _navigateToSelectedMovie.value = movie
}
fun displayMovieDetailsComplete() {
_navigateToSelectedMovie.value = null
}
Adapter:
class MainAdapter(private val onClickListener: OnClickListener) :
ListAdapter<Movie, MainAdapter.MovieViewHolder>(DiffCallback) {
class MovieViewHolder(private var binding: GridViewItemBinding):
RecyclerView.ViewHolder(binding.root ) {
fun bind(movie: Movie) {
binding.movie = movie
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
return oldItem.id == newItem.id
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
return MovieViewHolder(
GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)
)
)
}
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
val movie = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(movie)
}
holder.bind(movie)
}
class OnClickListener(val clickListener: (movie: Movie) -> Unit) {
fun onClick(movie: Movie) = clickListener(movie)
}
}
Data binding adapters:
#BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<Movie>?) {
val adapter = recyclerView.adapter as MainAdapter
adapter.submitList(data)
}
#BindingAdapter("posterImageUrl")
fun bindPosterImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = (IMAGE_BASE_URL + POSTER_SIZE + imgUrl).toUri().buildUpon().build()
Glide.with(imgView.context)
.load(imgUri)
.apply(RequestOptions().placeholder(R.drawable.loading_animation).error(R.drawable.ic_broken_image))
.into(imgView)
}
}
I now data new Live data appears in my View model, but the Recyclerview doesn't get updated until I swipe to refresh. The viewmodel instance is passed to the recyclerview via databinding. I now in fragments we call the observe method on Live data, but don't know how its is done in this example. In the Mars real estate example it appears to be working. I cannot find where is the difference in my code.
Thanks in advance,
Armands
// in viewModel, is the type of movies LiveData<List<Movie>>?
val movies = getMoviesUseCase.allMovies()
If the type of movies returned from getMoviesUseCase.allMovies() is LivaData<List<Movie>> type, the observers(Recyclerview) can receive the changes.
If the type of movies is List<Movie>, the observers cannot receive the changes, because no one to notify the observers. You should, in this case, change it to the LiveData<List<Movie>>.
private val _movies = MutableLiveData<List<Movie>>().apply { value = emptyList() }
val movies: LiveData<List<Movie>> = _movies
and try to init the _movies like this:
viewModelScope.launch {
_movies.value = getMoviesUseCase.allMovies()
}

Categories

Resources