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
}
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.
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)
}
}
For some reason, the second parameter value for both binding Adapters always returns null and I cannot figure out why. I am selecting a plantIndividual from a RecyclerView in the overview fragment and using it to navigate to a details page - individual fragment. Both Fragments share a viewModel.
Here are my BindingAdapters:
#BindingAdapter("listPhotoData")
fun bindPlantRecyclerView(recyclerView: RecyclerView,
data: List<PlantPhoto>?) {
val adapter = recyclerView.adapter as CollectionIndividualAdapter
adapter.submitList(data)
}
#BindingAdapter("singleImage")
fun loadImage(imgView: ImageView, imgUrl: File) {
imgUrl.let {
Glide.with(imgView.context)
.load(imgUrl)
.apply(
RequestOptions()
.placeholder(R.drawable.loading_animation)
.error(R.drawable.ic_broken_image))
.into(imgView)
}
}
My details fragment 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.collection.presentation.overview.CollectionOverviewViewModel" />
<variable
name="plantPhoto"
type="com.example.storage.data.PlantPhoto" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<ImageView
android:id="#+id/collection_individual_imageview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:singleImage="#={viewModel.plantPhotoDisplay.plantFilePath}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.26999998"
tools:srcCompat="#tools:sample/avatars" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/collection_individual_recyclerview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/collection_individual_imageview"
app:layout_constraintVertical_bias="0.498"
app:listPhotoData="#={viewModel.listPlantPhoto}"
tools:listitem="#layout/image_plant_photo_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ViewModel:
class CollectionOverviewViewModel(application: Application) : AndroidViewModel(application) {
lateinit var mediaPlantList: MutableList<File>
private var newPhotoList = mutableListOf<PlantPhoto>()
private val context = getApplication<Application>().applicationContext
private val _navigateToSelectedPlant = MutableLiveData<PlantIndividual>()
val navigateToSelectedPlant: LiveData<PlantIndividual>
get() = _navigateToSelectedPlant
private val _listPlantPhoto = MutableLiveData<MutableList<PlantPhoto>>()
val listPlantPhoto: LiveData<MutableList<PlantPhoto>>
get() = _listPlantPhoto
private val _plantPhotoDisplay = MutableLiveData<PlantPhoto>()
val plantPhotoDisplay: LiveData<PlantPhoto>
get() = _plantPhotoDisplay
fun displayPlantDetails(plantIndividual: PlantIndividual) {
_navigateToSelectedPlant.value = plantIndividual
}
fun displayPlantDetailsComplete() {
_navigateToSelectedPlant.value = null
}
fun retrievePlantList(plantIndividual: PlantIndividual) {
val dataClassNum = plantIndividual.plantId
viewModelScope.launch {
mediaPlantList = context?.getExternalFilesDir("planio/dataclasses/$dataClassNum")
?.listFiles()?.sortedDescending()?.toMutableList() ?: mutableListOf()
}
}
fun changeToPlantPhotos(plantList: MutableList<File>) {
plantList.map {
val file = FileInputStream(it)
val inStream = ObjectInputStream(file)
val item = inStream.readObject() as PlantPhoto
newPhotoList.add(item)
}
_plantPhotoDisplay.value = newPhotoList.last()
_listPlantPhoto.value = newPhotoList
}
}
OverView Fragment from which I am selecting a plantIndividual from a RecyclerView and navigating to a details page:
viewModel.navigateToSelectedPlant.observe(viewLifecycleOwner, {
if (null != it) {
viewModel.retrievePlantList(it)
viewModel.changeToPlantPhotos(viewModel.mediaPlantList)
this.findNavController().navigate(
CollectionOverviewFragmentDirections.
actionCollectionOverviewFragmentToCollectionIndividualFragment(it))
}
})
Details Fragment:
class CollectionIndividualFragment: Fragment() {
private lateinit var binding: FragmentCollectionIndividualBinding
private val viewModel: CollectionOverviewViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_collection_individual, container,
false)
binding.toCollectionOverview.setOnClickListener {
this.findNavController().navigate(CollectionIndividualFragmentDirections.
actionCollectionIndividualFragmentToCollectionOverviewFragment())
viewModel.displayPlantDetailsComplete()
}
binding.viewModel = viewModel
binding.lifecycleOwner = this
binding.collectionIndividualRecyclerview.adapter = CollectionIndividualAdapter()
binding.plantPhoto = viewModel.plantPhotoDisplay.value
return binding.root
}
I guess you're setting the lifecycleOwner after setting the viewModel of your binding and as a result, after viewModel is set in binding, it cannot observe live data because the lifecycleOwner is null at that point. I suggest to set it after setting binding
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_collection_individual, container,
false)
binding.lifecycleOwner = this
Edit:
Also don't forget to use by activityViewModels instead of by viewModels to share viewModel among your fragments
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.
I might be doing this all wrong, but I have the same exact implementation in another fragment/viewmodel with no problems. Maybe because it's a dialog? Every time I log message or message.messagebody it returns null. Can anyone maybe point out why? Currently learning mvvm.
xml: (the important bit since it's long)
<?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>
<variable
name="user"
type="com.catbellystudio.knodee.models.Users" />
<variable
name="vm"
type="com.catbellystudio.knodee.ui.profile.ProfileViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="400dp"
android:layout_margin="10dp"
android:background="#drawable/custom_background_popup"
android:elevation="10dp"
android:orientation="vertical">
<ScrollView
android:id="#+id/popupTextLayout"
android:layout_width="match_parent"
android:layout_height="277dp"
android:layout_marginTop="8dp">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorPrimary"
android:hint="#string/your_message"
android:inputType="textMultiLine"
android:padding="10dp"
android:text="#{vm.message.messageBody}" />
</ScrollView>
</LinearLayout>
</layout>
viewmodel:
class ProfileViewModel(
private val userRepository: UserRepository,
private val messageRepository: MessageRepository
) : ViewModel() {
var message: Message = Message()
var sender: Users? = null
var receiver: Users? = null
var string:String?=null
fun getLoggedInUser() = runBlocking { userRepository.getUser() }
fun onBackPressed(view: View) {
Navigation.findNavController(view).navigateUp()
}
fun postMessage(view:View) {
Coroutines.main {
Log.e("messagevm", message.toString())
}
}
}
fragment:
class MessageFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ProfileViewModel
private lateinit var profileBinding: FragmentProfileBinding
private lateinit var popupBinding: FragmentPopupBinding
override val kodein by kodein()
private val factory: ProfileViewModelFactory by instance()
private lateinit var dialog: Dialog
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProviders.of(this, factory).get(ProfileViewModel::class.java)
profileBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_profile,
container,
false
)
popupBinding = FragmentPopupBinding.inflate(LayoutInflater.from(context))
dialog = context?.let { Dialog(it) }!!
dialog.setContentView(popupBinding.root)
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
profileBinding.viewModel = viewModel
popupBinding.vm = viewModel
getSender()
return profileBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsButtonProfile.visibility = View.GONE
messageButtonProfile.setOnClickListener {
showPopUp()
}
val receiver by lazy {
arguments?.let { fromBundle(it).user }
}
viewModel.receiver = receiver
}
private fun showPopUp() {
dialog.show()
val switch = dialog.visibilitySwitchPopup
val visibilityTextView = dialog.visibilityTextViewPopup
dialog.closeButtonPopUp?.setOnClickListener {
dialog.dismiss()
}
switch?.setOnClickListener {
val isIconEnabled = switch.isIconEnabled
if (isIconEnabled) {
visibilityTextView?.text = getString(R.string.anonymous_prompt)
} else {
visibilityTextView?.text = getString(R.string.anonymous_warning)
}
switch.switchState()
}
}
private fun getSender() {
viewModel.getLoggedInUser()?.let { viewModel.sender = it }
}
}
Any help would be appreciated!
move this "popupBinding.vm = viewModel" line to onViewCreated() method and also include this line "popupBinding.lifeCycleOwner=this" in same method
Solved by changing
android:text="#{vm.message.messageBody}
to
android:text="#={vm.message.messageBody}