I have this app in which I have a parent fragment, which has 2 child fragments in a ViewPager2 object.
One of the child fragments has an interface to communicate changes on its menu to the parent fragment.
The child fragment in question is TasksListFragment.kt and the parent fragment is TodayFragment.kt
When I try to initialize the interface in the child fragment onAttach() function, I get
FATAL EXCEPTION: main Process: com.rajchenbergstudios.hoygenda, PID:
java.lang.ClassCastException: com.rajchenbergstudios.hoygenda.ui.activity.MainActivity cannot be cast to com.rajchenbergstudios.hoygenda.ui.todaylists.taskslist.TasksListFragment$ChildFragmentListener
I don't understand why I get this error, and it says MainActivity, when the parent is a fragment which is the one implementing the interface in the first place, not the MainActivity.
I have everything set up correctly:
- I have an interface in the child fragment
- The interface is used on the child fragment onCreateMenu to pass the menu object to its interface function
onFragmentMenuChanged(menu: Menu)
- I override the child fragment's
onAttach()
and initialize the interface:
override fun onAttach(context: Context) {
super.onAttach(context)
childFragmentListener = context as ChildFragmentListener
}
- I write a function called
setListener()
which is called from the parent fragment to pass its contextthis
to the function parameter which assigns it to the childFragment listener
fun setListener(listener: ChildFragmentListener) {
this.childFragmentListener = listener
}
- The parent fragment implements the child fragment listener as seen in the TodayFragment.kt file
Can you tell me what am I doing wrong or how to implement an interface to effectively communicate from child fragment back to its parent fragment?
TasksListFragment.kt
@ExperimentalCoroutinesApi
@AndroidEntryPoint
class TasksListFragment : Fragment(R.layout.fragment_child_tasks_list), TasksListAdapter.OnItemClickListener {
private val viewModel: TasksListViewModel by viewModels()
private lateinit var searchView: SearchView
private lateinit var childFragmentListener: ChildFragmentListener
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentChildTasksListBinding.bind(view)
val tasksListAdapter = TasksListAdapter(this)
binding.apply {
tasksListRecyclerview.layoutTasksListRecyclerview.apply {
adapter = tasksListAdapter
layoutManager = LinearLayoutManager(requireContext())
setHasFixedSize(true)
}
ItemTouchHelper(object: ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT){
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val task = tasksListAdapter.currentList[viewHolder.adapterPosition]
viewModel.onTaskSwiped(task)
}
}).attachToRecyclerView(tasksListRecyclerview.layoutTasksListRecyclerview)
}
loadObservable(binding, tasksListAdapter)
loadTasksEventCollector()
loadMenu()
}
private fun loadObservable(binding: FragmentChildTasksListBinding, tasksListAdapter: TasksListAdapter) {
viewModel.tasks.observe(viewLifecycleOwner){ tasksList ->
binding.apply {
HGDAViewStateUtils.apply {
if (tasksList.isEmpty()) {
setViewVisibility(tasksListRecyclerview.layoutTasksListRecyclerview, visibility = View.INVISIBLE)
setViewVisibility(tasksListLayoutNoData.layoutNoDataLinearlayout, visibility = View.VISIBLE)
} else {
setViewVisibility(tasksListRecyclerview.layoutTasksListRecyclerview, visibility = View.VISIBLE)
setViewVisibility(tasksListLayoutNoData.layoutNoDataLinearlayout, visibility = View.INVISIBLE)
tasksListAdapter.submitList(tasksList)
}
}
}
}
}
/**
* TasksListViewModel.TaskEvent.ShowUndoDeleteTaskMessage: Stays in this class. It asks for components relevant to this class.
* TasksListViewModel.TaskEvent.NavigateToEditTaskScreen: Stays in this class. The method it overrides comes from task list adapter.
* TasksListViewModel.TaskEvent.NavigateToDeleteAllCompletedScreen: Stays in this class. Relevant to menu which is in this class.
* TasksListViewModel.TaskEvent.NavigateToDeleteAllScreen: Stays in this class. Relevant to menu which is in this class.
*/
private fun loadTasksEventCollector() {
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.tasksEvent.collect { event ->
when (event) {
is TasksListViewModel.TaskEvent.ShowUndoDeleteTaskMessage -> {
Snackbar
.make(requireView(), "Task deleted", Snackbar.LENGTH_LONG)
.setAction("UNDO"){
viewModel.onUndoDeleteClick(event.task)
}
.show()
}
is TasksListViewModel.TaskEvent.NavigateToEditTaskScreen -> {
val action = TodayFragmentDirections
.actionTodayFragmentToTaskAddEditFragment(task = event.task, title = "Edit task", taskinset = null, origin = 1)
findNavController().navigate(action)
}
is TasksListViewModel.TaskEvent.NavigateToAddTaskToSetBottomSheet -> {
val action = TasksListFragmentDirections.actionGlobalSetBottomSheetDialogFragment(task = event.task, origin = 1)
findNavController().navigate(action)
}
is TasksListViewModel.TaskEvent.NavigateToDeleteAllCompletedScreen -> {
val action = TasksListFragmentDirections
.actionGlobalTasksDeleteAllDialogFragment(origin = 1)
findNavController().navigate(action)
}
is TasksListViewModel.TaskEvent.NavigateToDeleteAllScreen -> {
val action = TasksListFragmentDirections
.actionGlobalTasksDeleteAllDialogFragment(origin = 3)
findNavController().navigate(action)
}
}.exhaustive
}
}
}
private fun loadMenu(){
val menuHost: MenuHost = requireActivity()
menuHost.addMenuProvider(object: MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
childFragmentListener.onFragmentChanged(menu)
menuInflater.inflate(R.menu.menu_tasks_list_fragment, menu)
val searchItem = menu.findItem(R.id.tasks_list_menu_search)
searchView = searchItem.actionView as SearchView
val pendingQuery = viewModel.searchQuery.value
if (pendingQuery != null && pendingQuery.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(pendingQuery, false)
}
searchView.OnQueryTextChanged{ searchQuery ->
viewModel.searchQuery.value = searchQuery
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
menu.findItem(R.id.tasks_list_menu_hide_completed).isChecked =
viewModel.preferencesFlow.first().hideCompleted
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.tasks_list_menu_sort_by_date -> {
viewModel.onSortOrderSelected(SortOrder.BY_DATE)
true
}
R.id.tasks_list_menu_sort_by_name -> {
viewModel.onSortOrderSelected(SortOrder.BY_NAME)
true
}
R.id.tasks_list_menu_hide_completed -> {
menuItem.isChecked = !menuItem.isChecked
viewModel.onHideCompletedSelected(menuItem.isChecked)
true
}
R.id.tasks_list_menu_delete_completed -> {
viewModel.onDeleteAllCompletedClick()
true
}
R.id.tasks_list_menu_delete_all -> {
viewModel.onDeleteAllClick()
true
}
else -> false
}
}
}, viewLifecycleOwner, Lifecycle.State.RESUMED)
}
interface ChildFragmentListener {
fun onFragmentChanged(menu: Menu)
}
fun setListener(listener: ChildFragmentListener) {
this.childFragmentListener = listener
}
override fun onItemClick(task: Task) {
viewModel.onTaskSelected(task)
}
override fun onItemLongClick(task: Task) {
viewModel.onTaskLongSelected(task)
}
override fun onCheckboxClick(task: Task, isChecked: Boolean) {
viewModel.onTaskCheckedChanged(task, isChecked)
}
override fun onAttach(context: Context) {
super.onAttach(context)
childFragmentListener = context as ChildFragmentListener
}
override fun onPause() {
super.onPause()
Logger.i(TAG, "onPause", "TasksListFragment paused")
}
override fun onDestroyView() {
super.onDestroyView()
searchView.setOnQueryTextListener(null)
}
}
TodayFragment.kt
@ExperimentalCoroutinesApi
@AndroidEntryPoint
class TodayFragment : Fragment(R.layout.fragment_parent_today), TasksListFragment.ChildFragmentListener {
private val viewModel: TodayViewModel by viewModels()
private lateinit var binding: FragmentParentTodayBinding
private var fabClicked: Boolean = false
private lateinit var tasksListMenu: Menu
private lateinit var viewPager: ViewPager2
private val rotateOpen: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.rotate_open_anim) }
private val rotateClose: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.rotate_close_anim) }
private val fromBottom: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.from_bottom_anim) }
private val toBottom: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.to_bottom_anim) }
private val fadeIn: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.fade_in) }
private val fadeOut: Animation by lazy { AnimationUtils.loadAnimation(requireContext(), R.anim.fade_out) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentParentTodayBinding.bind(view)
binding.apply {
tasksListTransparentWhiteScreen.setOnClickListener {
fabAnimationsRollBack(binding)
fabClicked = !fabClicked
}
}
setChildFragmentMenus()
initViewPagerWithTabLayout(binding)
todayDateDisplay(binding)
initFabs(binding)
loadTodayEventCollector()
getFragmentResultListeners()
}
private fun setChildFragmentMenus(){
val tasksListFragment = TasksListFragment()
tasksListFragment.setListener(this)
Logger.i(TAG, "setChildFragmentMenus", "TasksListFragment menu set")
}
private fun getFragmentResultListeners() {
setFragmentResultListener("add_edit_request"){_, bundle ->
val result = bundle.getInt("add_edit_result")
onFragmentResult(result)
}
setFragmentResultListener("create_set_request_2"){_, bundle ->
val result = bundle.getInt("create_set_result_2")
onFragmentResult(result)
}
setFragmentResultListener("task_added_to_set_request"){_, bundle ->
val result = bundle.getInt("task_added_to_set_result")
val message = bundle.getString("task_added_to_set_message")
onFragmentResult(result, message)
}
setFragmentResultListener("task_added_from_set_request"){_, bundle ->
val result = bundle.getInt("task_added_from_set_result")
val message = bundle.getString("task_added_from_set_message")
onFragmentResult(result, message)
}
}
private fun onFragmentResult(result: Int, message: String? = ""){
viewModel.onFragmentResult(result, message)
}
/**
* TodayViewModel.TodayEvent.NavigateToAddTaskScreen: Relevant to this class. Belongs to Fab which are all in this class.
* TodayViewModel.TodayEvent.ShowTaskSavedConfirmationMessage: Relevant to this class. Belongs to onFragmentResultListener which is here.
* TodayViewModel.TodayEvent.ShowTaskSavedInNewOrOldSetConfirmationMessage: Relevant to this class. Belongs to onFragmentResultListener which is here.
* TodayViewModel.TodayEvent.ShowTaskAddedFromSetConfirmationMessage: Relevant to this class. Belongs to onFragmentResultListener which is here.
* TodayViewModel.TodayEvent.NavigateToAddTasksFromSetBottomSheet: Relevant to this class. Belongs to Fab which are all in this class.
*/
private fun loadTodayEventCollector() {
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.todayEvent.collect { event ->
when (event) {
is TodayViewModel.TodayEvent.NavigateToAddTaskScreen -> {
val action = TodayFragmentDirections
.actionTodayFragmentToTaskAddEditFragment(task = null, title = "Add task"
, taskinset = null, origin = 1)
findNavController().navigate(action)
}
is TodayViewModel.TodayEvent.ShowTaskSavedConfirmationMessage -> {
Snackbar.make(requireView(), event.msg, Snackbar.LENGTH_LONG).show()
setViewPagerPage(0)
}
is TodayViewModel.TodayEvent.ShowTaskSavedInNewOrOldSetConfirmationMessage -> {
Snackbar.make(requireView(), event.msg.toString(), Snackbar.LENGTH_LONG).show()
}
is TodayViewModel.TodayEvent.ShowTaskAddedFromSetConfirmationMessage -> {
Snackbar.make(requireView(), event.msg.toString(), Snackbar.LENGTH_LONG).show()
fabClicked = true
setFabAnimationsAndViewStates(binding)
setViewPagerPage(0)
}
is TodayViewModel.TodayEvent.NavigateToAddTasksFromSetBottomSheet -> {
val action = TasksListFragmentDirections
.actionGlobalSetBottomSheetDialogFragment(task = null, origin = 2)
findNavController().navigate(action)
}
}.exhaustive
}
}
}
// This will soon be used to be 1
private fun setViewPagerPage(index: Int){
viewModel.postActionWithDelay(300, object: TodayViewModel.PostActionListener{
override fun onDelayFinished() {
viewPager.setCurrentItem(index, true)
}
})
}
private fun todayDateDisplay(binding: FragmentParentTodayBinding) {
binding.apply {
tasksListDateheader.apply {
dateHeaderDayofmonth.text = viewModel.getCurrentDayOfMonth()
dateHeaderMonth.text = viewModel.getCurrentMonth()
dateHeaderYear.text = viewModel.getCurrentYear()
dateHeaderDayofweek.text = viewModel.getCurrentDayOfWeek()
}
}
}
private fun initViewPagerWithTabLayout(binding: FragmentParentTodayBinding) {
viewPager = binding.todayViewpager
val tabLayout: TabLayout = binding.todayTablayout
viewPager.adapter = activity?.let { TodayPagerAdapter(it) }
Logger.i(TAG, "initViewPagerWithTabLayout", "viewPager is not null")
TabLayoutMediator(tabLayout, viewPager) { tab, index ->
tab.text = when (index) {
0 -> "Tasks"
1 -> "Journal"
else -> throw Resources.NotFoundException("Tab not found at position")
}.exhaustive
when (index) {
0 -> {
}
1 -> {
fabClicked = false
}
}
}.attach()
}
private fun initFabs(binding: FragmentParentTodayBinding) {
binding.apply {
tasksListFab.setOnClickListener {
onMainFabClick(binding)
}
tasksListSubFab1.setOnClickListener {
Logger.i(TAG, "initFabs", "Coming soon")
}
tasksListSubFab2.setOnClickListener {
viewModel.onAddTasksFromSetClick()
}
tasksListSubFab3.setOnClickListener {
viewModel.onAddNewTaskClick()
}
}
}
private fun onMainFabClick(binding: FragmentParentTodayBinding) {
setFabAnimationsAndViewStates(binding)
}
private fun setFabAnimationsAndViewStates(binding: FragmentParentTodayBinding) {
setFabAnimationVisibilityAndClickability(binding, fabClicked)
fabClicked = !fabClicked
}
private fun setFabAnimationVisibilityAndClickability(binding: FragmentParentTodayBinding, clicked: Boolean) {
if (!clicked) fabAnimationsRollIn(binding) else fabAnimationsRollBack(binding)
}
private fun fabAnimationsRollIn(binding: FragmentParentTodayBinding) {
binding.apply {
HGDAAnimationUtils.apply {
HGDAViewStateUtils.apply {
setViewAnimation(v1 = tasksListFab, a = rotateOpen)
setViewAnimation(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3, a = fromBottom)
setViewAnimation(v1 = tasksListSubFab1Tv, v2 = tasksListSubFab2Tv, v3 = tasksListSubFab3Tv, a = fromBottom)
setViewAnimation(v1 = tasksListTransparentWhiteScreen, a = fadeIn)
setViewVisibility(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3
, v4 = tasksListSubFab1Tv, v5 = tasksListSubFab2Tv, v6 = tasksListSubFab3Tv, visibility = View.VISIBLE)
setViewVisibility(v1 = tasksListTransparentWhiteScreen, visibility = View.VISIBLE)
setViewClickState(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3, clickable = true)
setViewClickState(v1 = tasksListTransparentWhiteScreen, clickable = true)
}
}
}
}
private fun fabAnimationsRollBack(binding: FragmentParentTodayBinding) {
binding.apply {
HGDAAnimationUtils.apply {
HGDAViewStateUtils.apply {
setViewAnimation(v1 = tasksListFab, a = rotateClose)
setViewAnimation(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3, a = toBottom)
setViewAnimation(v1 = tasksListSubFab1Tv, v2 = tasksListSubFab2Tv, v3 = tasksListSubFab3Tv, a = toBottom)
setViewAnimation(v1 = tasksListTransparentWhiteScreen, a = fadeOut)
setViewVisibility(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3
, v4 = tasksListSubFab1Tv, v5 = tasksListSubFab2Tv, v6 = tasksListSubFab3Tv, visibility = View.INVISIBLE)
setViewVisibility(v1 = tasksListTransparentWhiteScreen, visibility = View.INVISIBLE)
setViewClickState(v1 = tasksListSubFab1, v2 = tasksListSubFab2, v3 = tasksListSubFab3, clickable = false)
setViewClickState(v1 = tasksListTransparentWhiteScreen, clickable = false)
}
}
}
}
override fun onFragmentChanged(menu: Menu) {
tasksListMenu = menu
}
override fun onPause() {
super.onPause()
tasksListMenu.clear()
}
}
CodePudding user response:
Fragment
is not a Context
i.e fragment is not a child of context
.
So when you try to cast context
as ChildFragmentListener
you are actually casting your Activity
to ChildFragmentListener
which is giving you this RuntimeException
. to make it work you can use childFragmentListener = parentFragment as ChildFragmentListener
Also if your Doing this you do not setListener
method anymore.
On other hand i would suggest you do not use listeners to communicate b/w fragments . I see you already using viewModel
so just use a shared one to communicate . You can get a shared ViewModel
inside child by creating it with parentFragment
.