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.
Related
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.
I have a nested recycler view structure. The data for the recycler view is coming from Room Database and it follows MVVM architecture with repositories and viewmodels.
I am able to inflate the parent recycler view and get the list on screen but the child recycler view is not being inflated. I can see the value being passed through the adapter by using logs. But that is not being used on onBindViewHolder() and neither onCreateViewHolder() is called
Any help would be appreciated.
My CODE:
Fragment :
class ControlPanelFragment : Fragment() {
private var _binding: FragmentControlPanelBinding? = null
private val binding: FragmentControlPanelBinding get() = _binding!!
private lateinit var controlPanelViewModel: ControlPanelViewModel
//floor adapter
private lateinit var adapter: FloorsAdapter
//rooms adapter
private lateinit var roomAdapter: RoomsAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentControlPanelBinding.inflate(layoutInflater)
controlPanelViewModel = ViewModelProvider(this)[ControlPanelViewModel::class.java]
adapter = FloorsAdapter()
roomAdapter = RoomsAdapter()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.rvFloors.adapter = adapter
binding.rvFloors.layoutManager = LinearLayoutManager(requireContext())
ListItemControlPanelFloorsBinding.inflate(layoutInflater).apply {
rvRoomControlPanel.adapter = roomAdapter
rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
Timber.d("Inside list item")
}
controlPanelViewModel.getAllFloors.observe(viewLifecycleOwner, Observer {
Timber.d("List is $it")
//Remove duplicates from received list
val distinct = it.toSet().toList().sorted()
Timber.d("List after removing duplicates and sorting: $distinct")
adapter.floorList(distinct)
for (i in distinct) {
controlPanelViewModel.getAllRooms(i).observe(viewLifecycleOwner, Observer { rooms ->
Timber.d("Floor: $i, Rooms: $rooms")
val distinctRooms = rooms.toSet().toList()
roomAdapter.roomList(distinctRooms)
})
}
})
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Parent recycler view adapter
open class FloorsAdapter() : RecyclerView.Adapter<FloorsAdapter.FloorViewHolder>() {
private var floorList = emptyList<String>()
inner class FloorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): FloorViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemControlPanelFloorsBinding.inflate(layoutInflater, parent, false)
return FloorViewHolder(binding.root)
}
#SuppressLint("NotifyDataSetChanged")
override fun onBindViewHolder(holder: FloorsAdapter.FloorViewHolder, position: Int) {
val item = floorList[position]
Timber.d("Current floor is $item, Floor List is : $floorList")
ListItemControlPanelFloorsBinding.bind(holder.itemView).apply {
Timber.d("Current floor is $item, Floor List is : $floorList")
tvFloor.text = "Floor : $item"
}
}
#SuppressLint("NotifyDataSetChanged")
fun floorList(floors: List<String>) {
this.floorList = floors
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return floorList.size
}
}
Child Recycler view adapter:
class RoomsAdapter() : RecyclerView.Adapter<RoomsAdapter.RoomsViewHolder>() {
private var roomList = emptyList<String>()
inner class RoomsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomsViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ListItemControlPanelRoomsBinding.inflate(inflater, parent, false)
return RoomsViewHolder(binding.root)
}
override fun onBindViewHolder(holder: RoomsViewHolder, position: Int) {
Timber.d("Room List onBindViewHolder: $roomList")
val item = roomList[position]
ListItemControlPanelRoomsBinding.bind(holder.itemView).apply {
tvRoom.text = item
}
}
override fun getItemCount(): Int {
return roomList.size
}
#SuppressLint("NotifyDataSetChanged")
fun roomList(room: List<String>) {
this.roomList = room
Timber.d("Room List: $roomList")
notifyDataSetChanged()
}
}
Fragment layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".ui.controlPanel.ui.ControlPanelFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="10dp">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="#+id/switchFactory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="60dp"
android:checked="false"
android:text="Factory"
android:textOff="OFF"
android:textOn="ON"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Spinner
android:id="#+id/spnChooseFloor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="200dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="#+id/switchFactory"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/switchFactory"
app:layout_constraintTop_toTopOf="#+id/switchFactory" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="10dp"
android:elevation="8dp"
app:cardCornerRadius="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvFloors"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="#layout/list_item_control_panel_floors" />
</androidx.cardview.widget.CardView>
</LinearLayout>
List Item for fragment recycler view(parent recycler view)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical">
<TextView
android:id="#+id/tvFloor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:text="Floor 1"
android:textColor="#color/black"
android:textSize="20sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvRoomControlPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
tools:listitem="#layout/list_item_control_panel_rooms" />
</LinearLayout>
List item for child recycler view
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="40dp">
<TextView
android:id="#+id/tvRoom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:text="Conference Room"
android:textSize="20sp" />
</LinearLayout>
Edit 1:
My entity :
data class AddedDevicesInformation(
#ColumnInfo(name = "floor_name")
var floorName: String = "",
#ColumnInfo(name = "room_name")
var roomName: String = "",
#ColumnInfo(name = "machine_name")
var machineName: String = "",
#ColumnInfo(name = "device_name")
var deviceName: String = "",
#ColumnInfo(name = "factory_status")
var factoryStatus: Boolean? = false,
#PrimaryKey(autoGenerate = true)
var id: Int? = null,
) {
}
DAO query :
//returns all rooms associated with a floor
#Query("SELECT room_name from added_device_information where floor_name =:floor")
fun readAllRoomsOnAFloor(floor: String): LiveData<List<String>>
Repository:
class ControlPanelRepository(private val devicesInformationDao: DevicesInformationDao) {
fun getAllRooms(floor: String): LiveData<List<String>> = devicesInformationDao.readAllRoomsOnAFloor(floor)
ViewModel:
class ControlPanelViewModel(application: Application) : AndroidViewModel(application) {
//repository instance
val repository: ControlPanelRepository
//Variable for getting all floors
val getAllFloors: LiveData<List<String>>
init {
val database = PowerManagementDatabase.getDatabase(application)
val dao = database.getAddedDevicesInformationDao()
repository = ControlPanelRepository(dao)
getAllFloors = repository.getAllFloors
}
fun getAllRooms(floor: String): LiveData<List<String>> {
return repository.getAllRooms(floor)
}
}
The problem is that each "floor" (row in the floor RecyclerView) has its own unique "room" RecyclerView, but you never attach an adapter to those room RecyclerViews. Instead, you inflate an unused "floor" layout in the Fragment and attach a room adapter to it. That layout will never be displayed anywhere so its adapter is never used.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.rvFloors.adapter = adapter
binding.rvFloors.layoutManager = LinearLayoutManager(requireContext())
// The problem is this part.
// This view isn't shown anywhere, and the views inflated in FloorsAdapter
// never get a RoomAdapter attached
ListItemControlPanelFloorsBinding.inflate(layoutInflater).apply {
rvRoomControlPanel.adapter = roomAdapter
rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
Timber.d("Inside list item")
}
Each displayed floor layout is inflated in the floor adapter in onCreateViewHolder and the data for the floor then gets set in onBindViewHolder. This means you need to create a separate RoomsAdapter for each floor and attach it inside the floor adapter somewhere.
This makes accessing the room adapters more difficult with your current pattern, but if you make a Floor object that contains both its name and its list of rooms, then the floor object selected in onBindViewHolder can pass the list of rooms to its RoomAdapter. This means the FloorsAdapter would take a List<Floor> instead of List<String>. Then in the main fragment you only need to pass the list of Floor objects to the floor adapter and it will handle passing room lists on to the room adapters appropriately.
data class Floor(val name: String, val rooms: List<String>) {}
The floor adapter would look something like this:
// modify the ViewHolder to store the binding (so you don't have to
// re-bind the views so often) and store the room adapter for this
// floor. There should be one room adapter per floor.
inner class FloorViewHolder(val binding: ListItemControlPanelFloorsBinding,
val roomAdapter: RoomsAdapter)
: RecyclerView.ViewHolder(binding.root) {}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): FloorViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemControlPanelFloorsBinding.inflate(layoutInflater, parent, false)
// Create one adapter per floor here
val adapter = RoomsAdapter()
binding.rvRoomControlPanel.adapter = adapter
binding.rvRoomControlPanel.layoutManager = LinearLayoutManager(activity)
return FloorViewHolder(binding, adapter)
}
override fun onBindViewHolder(holder: FloorsAdapter.FloorViewHolder, position: Int) {
// for a given floor, set the rooms on that floor's room adapter
val currentFloor = floorList[position]
holder.roomAdapter.roomList(currentFloor.rooms)
holder.binding.tvFloor.text = "Floor : ${currentFloor.name}"
}
Here's an example of how that might look in the ViewModel, when it constructs the list of Floor objects, and the activity would observe the floorList LiveData. Each Floor contains all the data (including nested data) that it needs to display.
private val floorListLiveData = MutableLiveData<List<Floor>>()
val floorList: LiveData<List<Floor>>
get() = floorListLiveData
fun load() {
viewModelScope.launch {
val floors = mutableListOf<Floor>()
val floorList = dao.readFloors()
for(floorName in floorList) {
val rooms = dao.readRoomsInFloor(floorName)
floors.add(Floor(floorName, rooms))
}
floorListLiveData.postValue(floors)
}
}
I am trying to display a video Thumbnail in recyclerview with it is Title
here is my data class:
data class PropertyItem(
val id: Int,
val thumbnailUrl: String,
val title: String,
val videoId: String,
val videoUrl: String
)
and here is the list_item xml:
<ImageView
android:id="#+id/imageViewid"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintWidth_percent=".3"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:src="#drawable/ic_launcher_background"/>
MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var manager: RecyclerView.LayoutManager
private lateinit var myAdapter: RecyclerView.Adapter<*>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
manager = LinearLayoutManager(this)
getAllData()
}
fun getAllData(){
Api.retrofitService.getAllData().enqueue(object: Callback<List<PropertyItem>> {
override fun onResponse(
call: Call<List<PropertyItem>>,
response: Response<List<PropertyItem>>
) {
if(response.isSuccessful){
recyclerView = findViewById<RecyclerView>(R.id.recycler_view).apply{
myAdapter = MyAdapter(response.body()!!)
layoutManager = manager
adapter = myAdapter
}
}
}
override fun onFailure(call: Call<List<PropertyItem>>, t: Throwable) {
t.printStackTrace()
}
})
}
}
Adapter
class MyAdapter(private val data: List<PropertyItem>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
class MyViewHolder(val view: View): RecyclerView.ViewHolder(view){
fun bind(propertyItem: PropertyItem){
val title = view.findViewById<TextView>(R.id.tvTitle)
val imageView = view.findViewById<ImageView>(R.id.imageViewid)
title.text = propertyItem.title
Glide.with(view.context).load(propertyItem.thumbnailUrl).centerCrop().into(imageView)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
return MyViewHolder(v)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(data[position])
}
}
so I'm getting all the data from data class but it doesn't display the image and how to add url to the imageview so the user can click the image and redirected to the viedo?
I am following this tutorial:
https://www.codevscolor.com/android-kotlin-recyclerview-with-image
am I doing anything wrong?
this code solved my problem
Picasso.with(activity)
.load(countryList[position].thumbnailUrl)
.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE)
.networkPolicy(NetworkPolicy.NO_CACHE, NetworkPolicy.NO_STORE)
.into(holder.thumbnailUrl)
I had to use Picasso not Glide
I make an Android application in Kotlin, and i want to get some information from the webserver and put in a recycler view using data binding. I use repository pattern, so each time i make a get request, i save the information in room database and get data from it with live data. For some reason my ryclerview show nothing, and i get no errors. The request work and i get the data but the recyclerview not working.
My ViewModel:
class HomeViewModel(application: Application) : ViewModel() {
private val availableTicketsRepo = AvailableTicketsRepository(getInstance(application))
val tickets = availableTicketsRepo.availableTickets
init {
refreshDataFromRepository()
}
private fun refreshDataFromRepository() {
viewModelScope.launch {
try {
availableTicketsRepo.refreshAvailableTickets()
} catch (ex: Exception) {
ex.message?.let { Resource.error(data = null, message = it) }
Log.i("Home", "*** Eccezione: ${ex} ***")
}
}
}
My fragment class (with adapter and viewholder):
package com.example.ticketapp.ui.home
class HomeFragment : Fragment() {
private val viewModel: HomeViewModel by lazy {
val activity = requireNotNull(this.activity) {
"You can only access the viewModel after onActivityCreated()"
}
ViewModelProvider(this, HomeViewModel.Factory(activity.application))
.get(HomeViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = DataBindingUtil.inflate<FragmentHomeBinding>(
inflater,
R.layout.fragment_home,
container,
false
)
binding.lifecycleOwner = this
binding.viewModel = viewModel
binding.availableTicketsList.adapter = AvailableTicketsAdapter()
return binding.root
}
}
class AvailableTicketsAdapter() :
ListAdapter<AvailableTicket, AvailableTicketsAdapter.AvailableTicketViewHolder>(DiffCallback) {
class AvailableTicketViewHolder(private var binding: AvailableTicketItemsBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(tickets: AvailableTicket) {
binding.ticket = tickets
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<AvailableTicket>() {
override fun areItemsTheSame(oldItem: AvailableTicket, newItem: AvailableTicket): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(
oldItem: AvailableTicket,
newItem: AvailableTicket
): Boolean {
return oldItem.Id == newItem.Id
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvailableTicketViewHolder {
return AvailableTicketViewHolder(
AvailableTicketItemsBinding.inflate(
LayoutInflater.from(
parent.context
)
)
)
}
override fun onBindViewHolder(holder: AvailableTicketViewHolder, position: Int) {
val tickets = getItem(position)
holder.bind(tickets)
}
}
My repository:
class AvailableTicketsRepository(private val database: TicketDatabase) {
val availableTickets: LiveData<List<AvailableTicket>> =
Transformations.map(database.ticketDatabaseDao.getAvailableTickets()) {
it.asDomainModel()
}
suspend fun refreshAvailableTickets() {
withContext(Dispatchers.IO) {
val tickets = RetrofitBuilder.apiService.getAvailableTickets()
database.ticketDatabaseDao.insertAllAvailableTickets(tickets.map { it.asDataBaseModel() })
}
}
}
The xml for items in recylerview:
<?xml version="1.0" encoding="utf-8"?>
<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.view.View"/>
<variable
name="ticket"
type="com.example.ticketapp.model.AvailableTicket" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/titleText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLength="50"
android:text="#{ticket.title}"
android:singleLine="true"
android:textAlignment="center"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Titolo di max 50 caratteri per non uscire dalla riga" />
<TextView
android:id="#+id/customerText"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textSize="14sp"
android:text="#{ticket.customer}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/titleText"
tools:text="ESEMPIO DI UN REFERENTE" />
<TextView
android:id="#+id/dateText"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textAlignment="textEnd"
android:textSize="14sp"
android:text="#{ticket.date_Time}"
app:layout_constraintBaseline_toBaselineOf="#+id/customerText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/titleText"
tools:text="20/01/2021 10:00:00" />
<ImageView
android:id="#+id/warningImg"
style="#style/iconImg"
android:layout_marginTop="20dp"
android:visibility="#{ticket.triangle ? View.VISIBLE : View.GONE}"
android:foregroundGravity="center"
app:layout_constraintEnd_toStartOf="#+id/dateText"
app:layout_constraintStart_toEndOf="#+id/customerText"
app:layout_constraintTop_toBottomOf="#+id/titleText"
app:srcCompat="#drawable/ic_warning" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
XML of ryclerview:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/availableTicketsList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="10dp"
android:paddingEnd="10dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/toolbar"
tools:listitem="#layout/available_ticket_items" />
My Model class:
#Parcelize
data class AvailableTicket (
val Id: Int,
val Title: String,
val Description: String,
val Date_Time: String,
val Customer: String,
val Field: String,
val Category: String,
val Color: String,
val Author: String,
val Triangle: Boolean,
val MyPriority: String
): Parcelable
You should, notify adapter when data is modified, add a LiveData inside ViewModel and post it after availableTicketsRepo.refreshAvailableTickets(), then call submitList inside adapter once the data is received
ViewModel
class HomeViewModel(application: Application) : ViewModel() {
private val availableTicketsRepo = AvailableTicketsRepository(getInstance(application))
val tickets = availableTicketsRepo.availableTickets
init {
refreshDataFromRepository()
}
private fun refreshDataFromRepository() {
viewModelScope.launch {
try {
availableTicketsRepo.refreshAvailableTickets()
} catch (ex: Exception) {
ex.message?.let { Resource.error(data = null, message = it) }
Log.i("Home", "*** Eccezione: ${ex} ***")
}
}
}
}
Then in Fragment, submit list to adapter once fetched
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = DataBindingUtil.inflate<FragmentHomeBinding>(
inflater,
R.layout.fragment_home,
container,
false
)
binding.lifecycleOwner = this
binding.viewModel = viewModel
binding.availableTicketsList.adapter = AvailableTicketsAdapter()
viewModel.tickets.observe(this, Observer{
(binding.availableTicketsList.adapter as? AvailableTicketsAdapter)?.submitList(it)
})
return binding.root
}
I am a newbie with android databinding, but the ImageView doesn't bind in the RecyclerView. I have read several blogs but no luck. What am I missing?
Below are some of the blog posts I have read:
link 1
link2
Below is how I have styled my xml layout.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="movie"
type="com.movieapp.huxymovies.model.Result" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="#color/bg"
android:orientation="vertical">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:layout_marginTop="8dp"
android:background="#color/bg"
android:orientation="horizontal">
<ImageView
android:id="#+id/img"
android:layout_width="70dp"
android:layout_height="100dp"
android:layout_marginLeft="8dp"
app:movieImage="#{movie.MPosterPath}" />
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
</LinearLayout>
</layout>
Then this is the modal class which contains all the attributes:
#Entity(tableName = "Results")
class Result {
companion object {
#JvmStatic
#BindingAdapter("movieImage")
fun LoadImage(view: View, mPosterPath: String?) {
val imageView = view as ImageView
Glide.with(view.context)
.load(Utils.IMAGE_BASE_URL + mPosterPath)
.into(imageView)
}
#BindingAdapter("rating")
fun setRating(ratingBar: RatingBar, rating: Float) {
if (rating != null) {
ratingBar.rating = rating
}
}
}
constructor(mId: Long?, mOverview: String?, mPosterPath: String?, mTitle: String?, mVoteAverage: Double?) {
this.mId = mId
this.mOverview = mOverview
this.mPosterPath = mPosterPath
this.mTitle = mTitle
this.mVoteAverage = mVoteAverage
}
constructor()
#PrimaryKey
#SerializedName("id")
var mId: Long? = null
#SerializedName("overview")
var mOverview: String? = null
#SerializedName("poster_path")
var mPosterPath: String? = null
#SerializedName("title")
var mTitle: String? = null
#SerializedName("vote_average")
var mVoteAverage: Double? = null
}
Then finally, in my adapter class, I tried to bind the item layout.
class ResultAdapter(private val context: Context) : PagedListAdapter<Result, ResultAdapter.ResultViewHolder>(DIFF_CALLBACK) {
public lateinit var mBinding: ItemActivitymainBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultViewHolder {
mBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.item_activitymain, parent, false)
return ResultViewHolder(mBinding)
}
override fun onBindViewHolder(holder: ResultViewHolder, position: Int) {
val result = getItem(position)
if (result != null) {
holder.itemActivitymainBinding.titleTxt.text = result.mTitle
}
}
class ResultViewHolder(itemView: ItemActivitymainBinding) : RecyclerView.ViewHolder(itemView.root) {
var itemActivitymainBinding: ItemActivitymainBinding
var root: View
init {
root = itemView.root
itemActivitymainBinding = itemView
}
}
companion object {
const val MOVIE_ID = "MOVIE_ID"
const val MOVIE_NAME = "MOVIE_NAME"
const val MOVIE_OVERVIEW = "MOVIE_OVERVIEW"
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Result>() {
override fun areItemsTheSame(oldItem: Result, newItem: Result): Boolean {
return oldItem.mId === newItem.mId
}
override fun areContentsTheSame(oldItem: Result, newItem: Result): Boolean {
return oldItem == newItem
}
}
}
}
Now I am still wondering why the image doesn't display because I have read some of the blog posts about this and I followed all their procedures.
First, you binding is missing its lifecycle owner (i.e., the activity or fragment in which you use the adapter). You should pass it to your adapter and then set it:
class ResultAdapter(private val lifecycleOwner: LifecycleOwner)
: PagedListAdapter<Result, ResultAdapter.ResultViewHolder>(DIFF_CALLBACK) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ItemActivitymainBinding>(inflater, R.layout.item_activitymain, parent, false)
// We set the lifecycle owner here
binding.setLifecycleOwner(lifecycleOwner)
return ResultViewHolder(binding)
}
...
}
// In your activity/fragment, pass the view as a parameter when creating the adapter
adapter = ResultAdapter(this)
(In the adapter, I have removed the property mBinding and the constructor parameter context, as neither of them were necessary.)
Second, you are defining the property movie in your layout, but you are not setting it with an actual value. To fix this, you have to update your implementation of onBindViewHolder():
override fun onBindViewHolder(holder: ResultViewHolder, position: Int) {
val movie = getItem(position)
// Here we set the layout variable "movie" with its corresponding value
holder.itemActivitymainBinding.movie = movie
}
(Please note that here I have removed the code you had written to change the title of your textview because you should change it through data-binding in the layout by doing this: android:text="#{movie.mTitle}".)
With these changes, your implementation should hopefully work!