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
Related
I am training with a simple app to show movies, I use an MVVM pattern and Flow.
Problem
This is my home, filterable through chips
I click on a movie , the details screen comes up then I go back to the home and this is the result:
Using logcat the home screen gets the list of movies to show but is not shown in the recyclerview (which uses diffUtil).
Below is the code for my fragment:
#AndroidEntryPoint
class Home2Fragment : Fragment() {
private val TAG = Home2Fragment::class.simpleName
private var _binding: FragmentHome2Binding? = null
private val binding: FragmentHome2Binding
get() = _binding!!
private val viewModel: HomeViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHome2Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
initChipGroupSpecificMovieList()
val adapter = MovieAdapter()
sectionRv.setHasFixedSize(true)
sectionRv.adapter = adapter
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.movieListBySpecification.collectLatest {
Log.d(TAG, "onViewCreated: received list")
adapter.addItems(it)
}
}
}
}
}
private fun FragmentHome2Binding.initChipGroupSpecificMovieList() {
val sortByMap = HomeViewModel.Companion.MovieListSpecification.values()
chipGroup.removeAllViews()
for (specification in sortByMap) {
val chip = Chip(context)
chip.isCheckable = true
chip.id = specification.ordinal
chip.text = getString(specification.nameResource)
chip.setOnCheckedChangeListener { _, isChecked ->
if (isChecked)
viewModel.setMovieListSpecification(specification)
}
chipGroup.addView(chip)
}
chipGroup.check(sortByMap.lastIndex - sortByMap.size + 1)//check first element
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
it seems at the line of code where I try to insert the list of movies in the adapter this doesn't add them because maybe via diffUtil it finds that it is the previous list and so it doesn't load it. However it doesn't show the previous one either, possible solutions?
as java code you can use this
#Override
public void onResume() {
super.onResume();
if(it.size() > 0) {
adapter.addItems(it)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_list, container, false)
val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireActivity())
noDataTextView = view.findViewById(R.id.no_data_textView)
noDataImageView = view.findViewById(R.id.no_data_imageView)
mToDoViewModel.getAllData.observe(viewLifecycleOwner, Observer { data ->
adapter.setData(data)
mSharedViewModel.checkIfDatabaseEmpty(data)
})
floatingActionButton = view.findViewById<FloatingActionButton>(R.id.floatingActionButton)
listLayout = view.findViewById(R.id.listLayout)
floatingActionButton.setOnClickListener {
findNavController().navigate(R.id.action_listFragment_to_addFragment)
}
//set menu
setHasOptionsMenu(true)
mSharedViewModel.emptyDatabase.observe(viewLifecycleOwner, Observer { data ->
showEmptyDatabaseViews(data)
})
return view
}
I have a visibility system going on where if the database is empty then the image is shown.
but when I run the code first image shows up then the data shows up then I debugged it and seen that mSharedViewModel.emptyDatabase.observe() function is running first? what is the main issue here,
ps, I am using suspended fun to load the data
Edit 1:
my default visibility is invisible
<ImageView>
.
.
android:visibility="invisible"
this is my ShareViewModel Class Which will check the database empty or not
class SharedViewModel(application: Application) : AndroidViewModel(application) {
val emptyDatabase: MutableLiveData<Boolean> = MutableLiveData(true)
fun checkIfDatabaseEmpty(toDoData: List<ToDoData>){
emptyDatabase.value=toDoData.isEmpty()
}
and this my ViewModel
class ToDoViewModel(application: Application):AndroidViewModel(application) {
private val toDoDao= ToDoDatabase.getDatabase(application).ToDoDao()
private val repository:ToDoRepository
val getAllData: LiveData<List<ToDoData>>
init {
repository=ToDoRepository(toDoDao)
getAllData=repository.getAllData
}
Your expectation:
I have a visibility system going on where if the database is empty then the image is shown.
According to your code:
android:visibility="invisible"
The default visibility is invisible okay but check the view model code
val emptyDatabase: MutableLiveData<Boolean> = MutableLiveData(true)
You set the value to true. So when any observer start observing the changes, the default value will be passed to the observer, so logically your code is OK, database is empty and image view is visible.
So, you should set false as the default value.
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.
Inside my fragment, i have some image and views that are getting their values by binding data, and beneath them a RecyclerView. The images and textviews are showing successfully but my Recyclerview won't show. If i return my view only, the RecyclerView shows but the binded data doesn't. I want to view both of them.
[]
class DetailFragment : Fragment(), LessonRecyclerAdapter.LessonItemListener {
private lateinit var viewModel: SharedViewModel
private lateinit var recyclerView: RecyclerView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_detail, container, false)
recyclerView = view.findViewById(R.id.lessonRecyclerView)
navController = Navigation.findNavController(requireActivity(), R.id.nav_host )
viewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
viewModel.lessonData.observe(viewLifecycleOwner, Observer {
val adapter =
LessonRecyclerAdapter(
it,
this
)
recyclerView.adapter = adapter
})
// return binding data
val binding = FragmentDetailBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
binding.viewModel = viewModel
return binding.root
//return view
}
As you can see, There are two inflates, one for binding and another for view(for recyclerview setup). The easy solution is to directly use recyclerview from binding variable to set up the list as:
private lateinit var binding: FragmentDetailBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
navController = Navigation.findNavController(requireActivity(), R.id.nav_host )
viewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
binding = FragmentDetailBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
binding.viewModel = viewModel
viewModel.lessonData.observe(viewLifecycleOwner, Observer {
val adapter =
LessonRecyclerAdapter(
it,
this
)
// directly access the view using ids
binding.lessonRecyclerView.adapter = adapter
})
return binding.root
}
Another option is to use binding adapters with live data to setup adapter and pass the data to the adapter.
I currently have an interface that's implemented in the MainActivity that allows communication with the NumberFragment. The onCountClicked is the method in NumberFragment receiving the id of the item click.
Question
How can I send the onCountClicked position to the NumberFragmentViewModel every time an item is clicked?
Note: Using databinding & recycleview
Main Activity
val newFragment = NumberFragment()
val args = Bundle()
args.putInt(NumberFragment().onCountClicked(data).toString(),data.toInt())
newFragment.arguments = args
Number Fragment
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val sleepTrackerViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(NumberViewModel::class.java)
setHasOptionsMenu(true)
binding.setLifecycleOwner(this)
binding.sleepTrackerViewModel = sleepTrackerViewModel
//Observes for any changes on database and updates the UI
sleepTrackerViewModel.dbCount.observe(this, Observer {dbCount ->
dbView.text = dbCount.number.toString()
})
return binding.root
}
fun onCountClicked(position: Long) {
Log.i("NumberF" , "------------->" + position)
}
You can just simply create a method in the viewModel that receives that position and updates the value of the live data. I would do it like this:
private val _idItemClicked = MutableLiveData<Int>()
val idItemClicked: LiveData<Int> = _idItemClicked
fun setIdItemClicked(id: Int) {
_idItemClicked.value = id
}
Now you call the function setIdItemClicked to set a new value.