I'm new to Android
And I'm having a problem with displaying the list items in the RecyclerView.
I was hoping to display the list one by one and append while it is still loading.
But what happens is, the list will appear after all the items are fetched.
Here is my Fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_lock_list, container, false)
val recyclerView = view.findViewById<RecyclerView>(R.id.list)
for(device in devices) {
fetchBuildingAddress(device)
}
if (recyclerView is RecyclerView) {
with(recyclerView) {
layoutManager = LinearLayoutManager(context)
adapter = deviceRecyclerViewAdapter
}
}
return view
}
private fun fetchBuildingAddress(device: Device) {
deviceWearableService.getBuilding(device.buildingId) {
when(it) {
is DeviceWearableServiceImpl.State.Success -> {
val buildingName = it.resources.buildingAddress.address1
val deviceBuilding = DeviceBuillding(device, buildingName)
deviceRecyclerViewAdapter.insertDevice(deviceBuilding)
}
else -> {
// TODO: ERROR STATE
}
}
}
}
And in my insertDevice
I simply call the notifyItemInserted in the adapter.
fun insertDevice(updateDeviceBuilding: DeviceBuillding) {
deviceBuilding.add(updateDeviceBuilding)
notifyItemInserted(deviceBuilding.size - 1 )
}
You could also suggest what is the better behavior! Thank you so much in advance.
It's unclear that you are fetching paged data or complete data , but what you need is to use flow with a Diff callback in your adapter i.e PagingDataAdapter(example) or ListAdatper(example).
Its implementation is quite easy you can find it on net.
Related
I have an app which creates "tasks" and creates a countdown for each one.
I have a view model with a list, and it's observable via LiveData
val tasksList = mutableListOf<Task>()
private val _tasksListData = MutableLiveData(tasksList)
val tasksListData : LiveData<MutableList<Task>>
get() = _tasksListData
fun addNewTask(task : Task){
tasksList.add(task)
_tasksListData.value = tasksList
}
I have already check that the items are created via a log statement. So that's working alright.
Then in the fragment I'm observing this live data and trying to add dynamically each task.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_history,
container,
false
)
taskListContainer = binding.tasksListContainer
tasksViewModel = ViewModelProvider(requireActivity()).get(TasksViewModel::class.java)
//Checks if the list has some items, otherwise displays a message
checkTasksList()
tasksViewModel.tasksListData.observe(viewLifecycleOwner,{
for (item in it) {
val view: IndividualTaskViewBinding = DataBindingUtil.inflate(
inflater, R.layout.individual_task_view, container, false
)
view.taskTitle.text = item.name
view.taskDateCreated.text = item.dateCreated
view.taskTertiaryText.text = item.cyclesCompleted.toString()
taskListContainer.addView(view.root)
}
checkTasksList()
})
setHasOptionsMenu(true)
return binding.root
}
private fun checkTasksList(){
if(taskListContainer.childCount == 0 ){
binding.emptyListText.setVisibility(View.VISIBLE)
} else{
binding.emptyListText.setVisibility(View.GONE)
}
}
}
The problem is that onCreateView() is called just once, and then I can't find a way to Observe the LiveData again. I have checked with Log statements, but while I'm swapping tabs (meaning I'm swapping fragments), I can't get any method from the Fragment's lifecycle.
The entire project is here: https://github.com/arieldipietro/PomodoroTechnique
My recyclerview doesn't udpate with the livedata when I remove an
item.
The function jokeisclicked() opens up a dialog for the user which
can choose to edit or delete an item of the room database.
The delete completes, but only when I refresh the tab. How can I complete the delete without refreshing the tab?
class DashboardFragment : Fragment() {
private lateinit var dashboardViewModel: DashboardViewModel
lateinit var binding: FragmentDashboardBinding
lateinit var adapter: JokeRecyclerViewAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//init repo and viewmodel
val dao: JokeDao = RoomDB.getInstance(requireContext()).JokeDAO
val jokerepo = JokeRepo(dao, RetrofitBuilder.jokeservice)
val factory = DashboardViewModelFactory(jokerepo)
dashboardViewModel = ViewModelProvider(this, factory).get(DashboardViewModel::class.java)
//init binding
binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
//recycler observe from livedata
dashboardViewModel.jokes.observe(viewLifecycleOwner, {
binding.recyclerview.layoutManager = LinearLayoutManager(this.requireContext())
adapter = JokeRecyclerViewAdapter(it, { selected: Joke -> jokeIsClicked(selected) })
binding.recyclerview.adapter = adapter
})
return binding.root
}
fun jokeIsClicked(joke: Joke) {
//show pop up dialog
val dialog = PopUpFragment(joke)
getFragmentManager()?.let { dialog.show(it, "popUpDialog") }
}
}
here is the popupfragment class
class PopUpFragment(private val selectedJoke : Joke) : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
//init viewmodel
val dao = RoomDB.getInstance(this.requireContext()).JokeDAO
val jokerepo = JokeRepo(dao,RetrofitBuilder.jokeservice)
val factory = PopUpViewModelFactory(jokerepo)
val popupViewModel : PopUpFragmentViewModel = ViewModelProvider(this,factory).get(PopUpFragmentViewModel::class.java)
//init binding
val binding = FragmentPopUpBinding.inflate(inflater,container,false)
//edit clicked
binding.editjoke.setOnClickListener{
}
//delete clicked
binding.deletejoke.setOnClickListener {
//TODO
popupViewModel.deleteJoke(selectedJoke)
this.dismiss()
}
return binding.root
}
}
I think you dont update value of livedata at viewModel. Could you share viewmodles also ? If dialogViewModel removes joke from database than you have to return at #Dao's method LiveData or Flow.
Any way don't set adapert and layout manager at observe method - it's point less. For updating list at adaper you could create method and call in it notifiDataSetChanged() or (the best way of handling changes at adaper) diffUtils.
You'll need to do 2 things:
1 - Remove the data from your variable i.e if you're storying it within a an array then use array = ArrayUtils.remove(array, index);
2 - run .notifyDataSetChanged() on your RecyclerView's Adapter in order refresh the view based on the values of the new data set.
I have a fragment called MainFragment that contains a ViewPager that contains another fragment called LibraryFragment.
LibraryFragment contains a RecyclerView with a list of items that contain an ImageView. The ImageView's contents are loaded with Coil.
When an item is clicked, LibraryFragment navigates to another fragment called ArtistDetailFragment and uses the ImageView from the item.
The problem is that while the enter transition works fine, the ImageView does not return to the list item when navigating back and only fades away. Ive attached an example below:
Ive tried using postponeEnterTransition() and startPostponedEnterTransition() along with adding a SharedElementCallback but neither have worked that well. I've also ruled out Coil being the issue.
Heres LibraryFragment:
class LibraryFragment : Fragment() {
private val musicModel: MusicViewModel by activityViewModels()
private val libraryModel: LibraryViewModel by activityViewModels()
private lateinit var binding: FragmentLibraryBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLibraryBinding.inflate(inflater)
binding.libraryRecycler.adapter = ArtistAdapter(
musicModel.artists.value!!,
BindingClickListener { artist, itemBinding ->
navToArtist(artist, itemBinding)
}
)
return binding.root
}
private fun navToArtist(artist: Artist, itemBinding: ItemArtistBinding) {
// When navigation, pass the artistImage of the item as a shared element to create
// the image popup.
findNavController().navigate(
MainFragmentDirections.actionShowArtist(artist.id),
FragmentNavigatorExtras(
itemBinding.artistImage to itemBinding.artistImage.transitionName
)
)
}
}
Heres ArtistDetailFragment:
class ArtistDetailFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentArtistDetailBinding.inflate(inflater)
sharedElementEnterTransition = TransitionInflater.from(requireContext())
.inflateTransition(android.R.transition.move)
val musicModel: MusicViewModel by activityViewModels()
val artistId = ArtistDetailFragmentArgs.fromBundle(requireArguments()).artistId
// Get the transition name used by the recyclerview ite
binding.artistImage.transitionName = artistId.toString()
binding.artist = musicModel.artists.value?.find { it.id == artistId }
return binding.root
}
}
And heres the RecyclerView Adapter/ViewHolder:
class ArtistAdapter(
private val data: List<Artist>,
private val listener: BindingClickListener<Artist, ItemArtistBinding>
) : RecyclerView.Adapter<ArtistAdapter.ViewHolder>() {
override fun getItemCount(): Int = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemArtistBinding.inflate(LayoutInflater.from(parent.context))
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
// Generic ViewHolder for an artist
inner class ViewHolder(
private val binding: ItemArtistBinding
) : RecyclerView.ViewHolder(binding.root) {
// Bind the view w/new data
fun bind(artist: Artist) {
binding.artist = artist
binding.root.setOnClickListener { listener.onClick(artist, binding) }
// Update the transition name with the new artist's ID.
binding.artistImage.transitionName = artist.id.toString()
binding.executePendingBindings()
}
}
}
EDIT: I used postponeEnterTransition and startPostponedEnterTransition like this:
// LibraryFragment
override fun onResume() {
super.onResume()
postponeEnterTransition()
// Refresh the parent adapter to make the image reappear
binding.libraryRecycler.adapter = artistAdapter
// Do the Pre-Draw listener
binding.libraryRecycler.viewTreeObserver.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
This only makes the RecyclerView itself refresh however, the shared element still fades away instead of returning to the RecyclerView item.
Chris Banes says;
You may wonder why we set the OnPreDrawListener on the parent rather than the view itself. Well that is because your view may not actually be drawn, therefore the listener would never fire and the transaction would sit there postponed forever. To work around that we set the listener on the parent instead, which will (probably) be drawn.
https://chris.banes.dev/fragmented-transitions/
try this;
change
// LibraryFragment
override fun onResume() {
super.onResume()
postponeEnterTransition()
// Refresh the parent adapter to make the image reappear
binding.libraryRecycler.adapter = artistAdapter
// Do the Pre-Draw listener
binding.libraryRecycler.viewTreeObserver.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
enter code here
to
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
postponeEnterTransition()
binding = FragmentLibraryBinding.inflate(inflater)
binding.libraryRecycler.adapter = ArtistAdapter(
musicModel.artists.value!!,
BindingClickListener { artist, itemBinding ->
navToArtist(artist, itemBinding)
}
)
(view?.parent as? ViewGroup)?.doOnPreDraw {
// Parent has been drawn. Start transitioning!
startPostponedEnterTransition()
}
return binding.root
}
So, what I'm trying to do is to get information from api and add it to model: MutableList and then set model information to recycler view. But the problem is, recycler view is being called before api gets all responses and so, my view is empty. This is what i concluded, if I'm wrong please correct me. But if not, How can I fix this problem?
class FavouritesFragment(myContext : HomeActivity, fvts: ArrayList<String>): Fragment() {
var activity = myContext
var favourites = fvts
var model : MutableList<CurrentWeatherModel> = mutableListOf()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_favourites, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
init()
}
private fun init() {
model.clear()
favourites.forEach {
getForecastModel(it)}
Log.d("other", model.size.toString())
favouritesRecyclerView.layoutManager = LinearLayoutManager(activity)
val adapter = CurrentRecyclerViewAdapter(model, activity)
favouritesRecyclerView.adapter = adapter
}
private fun getForecastModel(value: String?) {
if (!value.isNullOrEmpty()) {
DataLoader.getRequestForQuery("weather", value, "a77840cefd5a443793bd5e6b54776905", object: CustomCallback{
override fun onSuccess(result: String) {
if (result != "null") {
var converted = Gson().fromJson(result, CurrentWeatherModel::class.java)
model.add(converted)
}
}
})
}
}
}
You need to notify the adapter that data has changed, you can achieve this by calling adapter.notifyDataSetChanged() whenever the data has changed.
So you need to add this line after adding data to the list
model.add(converted)
adapter.notifyDataSetChanged()
I am trying to develop a Survey application for Android. In the requirements, the client is asking for a swipe effect between the questions. I used a ViewPager with FragmentPagerAdapter and everything works fine.
The problem comes when they require a Tree Decision System. In other words, when the surveyed person select an answer, I should lead him to a question according on which one is defined in that answer.
As you can see in the image, before answer the first question, I can't load the second page because I don't know which will be the question to load.
Also I can't know the number of pages that should I return in the getCount method, only when the user responds, I can know if there's one more page or not, and which should be its content.
I tried many solution posted over there, but the most important, or at least was logic for me. Is to set the count as the known pages number, and when the user select an answer, I tried to change the count and call notifyDataSetChanged method, but all what I get, is to change the number of pages dynamically, but not the content.
The scenario is:
In the first question, I set the count to 1, so the user can't swipe to the next page because it's unknown.
When the user select an answer, I change the count to 2 and load the next question. Everything OK!
If the user back to the first question and change his answer, I tried to destroy or change the content of the second page. But in this case, the notifyDataSetChanged doesn't replace the Fragment.
I know that I am asking a strange and difficult behavior, but I want to know if someone has to do the same thing and could find the right solution.
Sorry for don't post any code, but after so many intents, my code becomes ugly and I do revert in VCS.
You could try a linked list for your data items. Here's a quick example of how that might look.
var viewPager: ViewPager? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_dynamic_view_pager, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.viewPager)
viewPager?.apply {
adapter = MyAdapter(childFragmentManager, this).also {
it.initialDataItem = buildDataItems()
}
}
}
fun buildDataItems(): DataItem {
val item0 = DataItem(0).also {
it.title = "What can I help you with today?"
}
val item1 = DataItem(1).also {
it.title = "Technical Problem"
}
val item2 = DataItem(2).also {
it.title = "Ordering Problem"
}
val item3 = DataItem(3).also {
it.title = "Is your power light on?"
}
val item4 = DataItem(4).also {
it.title = "Have you received your order?"
}
val item5 = DataItem(5).also {
it.title = "New content node"
}
val item6 = DataItem(6).also {
it.title = "New content node"
}
item0.yesItem = item1
item0.noItem = item2
item1.yesItem = item3
item2.yesItem = item4
item3.yesItem = item5
item3.noItem = item6
return item0
}
data class DataItem(
var id: Int = 0
) {
var title: String = ""
var yesItem: DataItem? = null
var noItem: DataItem? = null
var answer: Answer = Answer.UNANSWERED
}
enum class Answer {
UNANSWERED,
YES,
NO
}
class MyAdapter(fm: FragmentManager, val viewPager: ViewPager) : FragmentPagerAdapter(fm) {
var initialDataItem: DataItem = DataItem()
override fun getItem(position: Int): Fragment {
var index = 0
var dataItem: DataItem? = initialDataItem
while (index < position) {
when (dataItem?.answer) {
Answer.YES -> dataItem = dataItem.yesItem
Answer.NO -> dataItem = dataItem.noItem
else -> {}
}
index ++
}
return DetailFragment(dataItem) {
dataItem?.answer = if (it) Answer.YES else Answer.NO
notifyDataSetChanged()
viewPager.setCurrentItem(position + 1, true)
}
}
override fun getCount(): Int {
var count = 1
var dataItem: DataItem? = initialDataItem
while (dataItem?.answer != Answer.UNANSWERED) {
when (dataItem?.answer) {
Answer.YES -> dataItem = dataItem.yesItem
Answer.NO -> dataItem = dataItem.noItem
else -> {}
}
count++
}
return count
}
}
class DetailFragment(val dataItem: DataItem?, val listener: ((answeredYes: Boolean) -> Unit)) : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_viewpager_detail, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<TextView>(R.id.title)?.text = dataItem?.title
view.findViewById<Button>(R.id.yesButton)?.setOnClickListener {
listener(true)
}
view.findViewById<Button>(R.id.noButton)?.setOnClickListener {
listener(false)
}
}
}
Note: You will want to refine this a bit to handle error cases and reaching the end of the tree. Also, you'll want to use a better method for injecting data into the detail fragment - this is just for illustration purposes.