I am developing an app that asks the user to type task's name, description....,but when the user intends to leave the edit page without saving the task I need to pop up a dialog to confirm leaving, so if he clicked abandon changes he must return to the home page of the app otherwise he will stay at the same page.
I have problem with this, when the user clicks abandon changes the dialog dismisses and he stays at the edit page.
MainActivity.kt
package muhabalafandi.example.tasktimer
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.content_main.*
private const val TAG = "MainActivity"
private const val DIALOG_ID_CANCEL_EDIT = 1
class MainActivity : AppCompatActivity(), AddEditFragment.OnSaveClicked,
MainActivityFragment.OnTaskEdit,
AppDialog.DialogEvents {
// Whether or the activity is in 2-pane mode
// i.e. running in landscape, or on a tablet.
private var mTwoPane = false
override fun onCreate(savedInstanceState: Bundle?) {
Log.d(TAG, "onCreate: starts")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
mTwoPane = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
Log.d(TAG, "onCreate: twoPane is $mTwoPane")
val fragment = findFragmentById(R.id.task_details_container)
if (fragment != null) {
// There was an existing fragment to edit a task, make sure the panes are set correctly
showEditPane()
} else {
task_details_container.visibility = if (mTwoPane) View.INVISIBLE else View.GONE
mainFragment.visibility = View.VISIBLE
}
Log.d(TAG, "onCreate: finished")
}
private fun showEditPane() {
task_details_container.visibility = View.VISIBLE
// hide the left hand pane, if in single pane view
mainFragment.visibility = if (mTwoPane) View.VISIBLE else View.GONE
}
private fun removeEditPane(fragment: Fragment? = null) {
Log.d(TAG, "removeEditPane called")
if (fragment != null) {
// supportFragmentManager.beginTransaction()
// .remove(fragment)
// .commit()
removeFragment(fragment)
}
// Set the visibility of the right hand pane
task_details_container.visibility = if (mTwoPane) View.INVISIBLE else View.GONE
// and show the left hand pane
mainFragment.visibility = View.VISIBLE
supportActionBar?.setDisplayHomeAsUpEnabled(false)
}
override fun onSaveClicked() {
Log.d(TAG, "onSaveClicked: called")
removeEditPane(findFragmentById(R.id.task_details_container))
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
when (item.itemId) {
R.id.menuMain_addTask -> taskEditRequest(null)
// R.id.menuMain_settings -> true
android.R.id.home -> {
Log.d(TAG, "onOptionsItemSelected: home button pressed")
val fragment = findFragmentById(R.id.task_details_container)
// removeEditPane(fragment)
if ((fragment is AddEditFragment) && fragment.isDirty()) {
showConfirmationDialog(
DIALOG_ID_CANCEL_EDIT,
getString(R.string.cancelEditDialogMessage),
R.string.cancelEditDialogPositiveCaption,
R.string.cancelEditDialogNegativeCaption
)
Log.d(TAG, "dialogId : $DIALOG_ID_CANCEL_EDIT")
} else {
removeEditPane(fragment)
}
}
}
return super.onOptionsItemSelected(item)
}
override fun onTaskEdit(task: Task) {
taskEditRequest(task)
}
private fun taskEditRequest(task: Task?) {
Log.d(TAG, "taskEditRequest: starts")
// Create a new fragment to edit the task
// val newFragment = AddEditFragment.newInstance(task)
// supportFragmentManager.beginTransaction()
// .replace(R.id.task_details_container, newFragment)
// .commit()
replaceFragment(AddEditFragment.newInstance(task), R.id.task_details_container)
showEditPane()
Log.d(TAG, "Exiting taskEditRequest")
}
override fun onBackPressed() {
val fragment = findFragmentById(R.id.task_details_container)
if (fragment == null || mTwoPane) {
super.onBackPressed()
} else {
// removeEditPane(fragment)
if ((fragment is AddEditFragment) && fragment.isDirty()) {
showConfirmationDialog(
DIALOG_ID_CANCEL_EDIT,
getString(R.string.cancelEditDialogMessage),
R.string.cancelEditDialogPositiveCaption,
R.string.cancelEditDialogNegativeCaption
)
} else {
removeEditPane(fragment)
}
}
}
override fun onPositiveDialogResult(dialogId: Int, args: Bundle) {
Log.d(TAG, "onPositiveDialogResult: called with dialogId $dialogId")
if (dialogId == DIALOG_ID_CANCEL_EDIT) {
val fragment = findFragmentById(R.id.task_details_container)
removeEditPane(fragment)
}
}
override fun onStart() {
Log.d(TAG, "onStart: called")
super.onStart()
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
Log.d(TAG, "onRestoreInstanceState: called")
super.onRestoreInstanceState(savedInstanceState)
}
override fun onResume() {
Log.d(TAG, "onResume: called")
super.onResume()
}
override fun onPause() {
Log.d(TAG, "onPause: called")
super.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
Log.d(TAG, "onSaveInstanceState: called")
super.onSaveInstanceState(outState)
}
override fun onStop() {
Log.d(TAG, "onStop: called")
super.onStop()
}
override fun onDestroy() {
Log.d(TAG, "onDestroy: called")
super.onDestroy()
}
}
AppDialog.kt
package muhabalafandi.example.tasktimer
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.DialogFragment
private const val TAG = "AppDialog"
const val DIALOG_ID = "id"
const val DIALOG_MESSAGE = "message"
const val DIALOG_POSITIVE_RID = "positive_rid"
const val DIALOG_NEGATIVE_RID = "negative_rid"
class AppDialog : DialogFragment() {
private var dialogEvents: DialogEvents? = null
internal interface DialogEvents {
fun onPositiveDialogResult(dialogId: Int, args: Bundle)
// fun onNegativeDialogResult(dialogId: Int, args: Bundle)
// fun onDialogCancelled(dialogId: Int)
}
override fun onAttach(context: Context) {
Log.d(TAG, "onAttach called: context is $context")
super.onAttach(context)
// Activities/Fragments containing this fragment must implement its callbacks.
dialogEvents = try {
// Is there a parent fragment? If so, that will be what we call back
parentFragment as? DialogEvents
} catch (e: TypeCastException) {
try {
// No parent fragment, so call back the Activity instead
context as DialogEvents
} catch (e: ClassCastException) {
// Activity doesn't implement the interface
throw ClassCastException("Activity $context must implement AppDialog.DialogEvents interface")
}
} catch (e: ClassCastException) {
// Parent fragment doesn't implement the interface
throw ClassCastException("Fragment $parentFragment must implement AppDialog.DialogEvents interface")
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
Log.d(TAG, "onCreateDialog called")
val builder = AlertDialog.Builder(requireContext())
// fix "smart cast to Bundle is impossible, because 'arguments' is a mutable property that could have been changed by this time"
val arguments = arguments
val dialogId: Int
val messageString: String?
var positiveStringId: Int
var negativeStringId: Int
if (arguments != null) {
dialogId = arguments.getInt(DIALOG_ID)
messageString = arguments.getString(DIALOG_MESSAGE)
if (dialogId == 0 || messageString == null) {
throw IllegalArgumentException("DIALOG_ID and/or DIALOG_MESSAGE not present in the bundle")
}
positiveStringId = arguments.getInt(DIALOG_POSITIVE_RID)
if (positiveStringId == 0) {
positiveStringId = R.string.ok
}
negativeStringId = arguments.getInt(DIALOG_NEGATIVE_RID)
if (negativeStringId == 0) {
negativeStringId = R.string.cancel
}
} else {
throw IllegalArgumentException("Must pass DIALOG_ID and DIALOG_MESSAGE in the bundle")
}
return builder.setMessage(messageString).setPositiveButton(positiveStringId) { _, _ ->
// callback positive result function
dialogEvents?.onPositiveDialogResult(dialogId, arguments)
}.setNegativeButton(negativeStringId) { _, _ ->
// callback negative result function, if you want to implement it.
// dialogEvents?.onNegativeDialogResult(dialogId, arguments)
}.create()
}
override fun onDetach() {
Log.d(TAG, "onDetach called")
super.onDetach()
// Reset the active callbacks interface, because we're no longer attached.
dialogEvents = null
}
override fun onCancel(dialog: DialogInterface) {
Log.d(TAG, "onCancel called")
val dialogId = requireArguments().getInt(DIALOG_ID)
// dialogEvents?.onDialogCancelled(dialogId)
}
}
buttonYes.setOnClickListener {
findNavController().popBackStack()
findNavController().popBackStack()
{
Related
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 context this 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()
}
}
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 need 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.
So I got a problem, When the Main Page is shown, it's supposed to call the API to get the data from it. But, the API never called. What I find strange is, that before Main Page, there is a login page that successfully called the API for login. But, on Main Page, the Retrofit/Okhttp never called the Page to get the data. Do you guys know what's wrong?
Here's my code screenshot:
StoryPageSource
StoryRepository
ViewModel
StoryAdapter
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var bind: ActivityMainBinding
private lateinit var storyAdapter: StoryAdapter
private val timelineStoryViewModel: TimelineStoryViewModel by viewModels {
ViewModelFactory(this, application)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bind = ActivityMainBinding.inflate(layoutInflater)
setContentView(bind.root)
showLoadingProcess(true)
displayStories()
bind.floatingActionButton.setOnClickListener {
startActivity(Intent(this, AddStoryActivity::class.java))
}
supportActionBar?.setTitle(R.string.app_name)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflaterMenu = menuInflater
inflaterMenu.inflate(R.menu.menus, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.app_bar_settings -> {
val context = this
val pref = context.getSharedPreferences(
R.string.token_pref.toString(),
Context.MODE_PRIVATE
)
val editor = pref.edit()
editor.remove(R.string.token.toString())
editor.remove(getString(R.string.email))
editor.remove(getString(R.string.password))
editor.apply()
startActivity(Intent(this, LoginActivity::class.java))
}
R.id.app_bar_maps -> {
startActivity(Intent(this, MapsActivity::class.java))
}
}
return super.onOptionsItemSelected(item)
}
private fun displayStories() {
showLoadingProcess(false)
storyAdapter = StoryAdapter()
bind.storyRV.layoutManager = LinearLayoutManager(this)
bind.storyRV.adapter = storyAdapter
timelineStoryViewModel.story().observe(this#MainActivity) { story ->
storyAdapter.submitData(lifecycle, story)
}
storyAdapter.setOnProfileCallback(object : StoryAdapter.OnProfileCallback {
override fun onProfileClicked(data: TimelineStory) {
chosenProfile(data)
}
})
}
private fun chosenProfile(story: TimelineStory) {
var name: TextView = findViewById(R.id.userName)
var storyPict: ImageView = findViewById(R.id.userImageStory)
val optionsCompat: ActivityOptionsCompat =
ActivityOptionsCompat.makeSceneTransitionAnimation(
this,
Pair(storyPict, getString(R.string.imageStoryDetail)),
Pair(name, getString(R.string.nama_pengguna)),
)
val sendData = Intent(this, StoryDetailActivity::class.java)
sendData.putExtra(StoryDetailActivity.EXTRA_STORY, story)
startActivity(sendData, optionsCompat.toBundle())
Toast.makeText(this, "Memuat Story " + story.name, Toast.LENGTH_SHORT).show()
}
private fun showLoadingProcess(isLoading: Boolean) {
if (isLoading) {
bind.loading.visibility = View.VISIBLE
} else {
bind.loading.visibility = View.GONE
}
}
}
I've been trying to search for an answer on my own but no one seems to be talking about my exact issue. It's usually a question about the RecyclerView Adapter Listener, and I've used that and it works every time.
When I run my code, I get this error,
java.lang.ClassCastException: com.wereworkingonit.communitystore.SettingsActivity#d180f18must implement ExampleDialogListener
which, without the try, catch block gives me
java.lang.ClassCastException: com.wereworkingonit.communitystore.SettingsActivity cannot be cast to com.wereworkingonit.communitystore.JoinStoreDialog$EngagedDialogListener, which doesn't make sense because I followed a tutorial and just translated it to Kotlin. I assume I am missing something about the difference in how Java and Kotlin.
My question is, what do I put in the listener = line in the OnAttach function?
This is where the issue comes from.
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = context as EngagedDialogListener
} catch (e: ClassCastException) {
throw ClassCastException(
context.toString() +
"must implement ExampleDialogListener"
)
}
}
This is the Parent Activity for the Dialog
package com.wereworkingonit.communitystore
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import com.wereworkingonit.communitystore.participant.ParticipantLandingPage
import com.wereworkingonit.communitystore.user.UserLandingPage
import com.wereworkingonit.communitystore.util.FirestoreUtil
private const val TAG = "SettingsActivity"
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity)
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, SettingsFragment())
.commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
class SettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener, JoinStoreDialog.EngagedDialogListener {
private lateinit var contextForPreferences: Context
private var engagedPreference: Boolean = false
private lateinit var sharedPref: SharedPreferences
private var switchPreference: SwitchPreferenceCompat? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
switchPreference = findPreference("engage")
androidx.preference.PreferenceManager.setDefaultValues(
contextForPreferences,
R.xml.root_preferences,
false
)
sharedPref =
androidx.preference.PreferenceManager.getDefaultSharedPreferences(
contextForPreferences
)
engagedPreference = sharedPref.getBoolean(AppConstants.ENGAGEMENT, false)
Log.d(TAG, "onCreate: $engagedPreference")
FirestoreUtil.getCurrentUser {
switchPreference!!.isChecked = it.engagement
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
contextForPreferences = context
}
override fun onStart() {
super.onStart()
androidx.preference.PreferenceManager.getDefaultSharedPreferences(contextForPreferences)
.registerOnSharedPreferenceChangeListener(this)
}
override fun onStop() {
super.onStop()
androidx.preference.PreferenceManager.getDefaultSharedPreferences(contextForPreferences)
.unregisterOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
engagedPreference = sharedPref.getBoolean(AppConstants.ENGAGEMENT, false)
Log.d(TAG, "onPause: $engagedPreference")
if (engagedPreference) {
//TODO: Add welcome splash for Engagement Change
val intent = Intent(contextForPreferences, ParticipantLandingPage::class.java)
startActivity(intent)
} else {
val intent = Intent(contextForPreferences, UserLandingPage::class.java)
startActivity(intent)
}
}
override fun notThisTime(nope: Boolean) {
FirestoreUtil.getCurrentUser {
switchPreference!!.isChecked = nope
}
}
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences?,
key: String?
) {
when (key) {
"engage" -> {
val joinStoreDialog = JoinStoreDialog()
if (switchPreference!!.isChecked) {
joinStoreDialog.show(childFragmentManager, "JoinStoreDialog")
} else {
FirestoreUtil.updateCurrentUser("", null, switchPreference!!.isChecked)
}
//TODO: One way trip, or what happens if the user clicks to end engagement?
}
}
}
}
}
This is the Dialog Fragment where the listener is made
package com.wereworkingonit.communitystore
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import androidx.appcompat.app.AppCompatDialogFragment
import com.wereworkingonit.communitystore.participant.ParticipantLandingPage
import com.wereworkingonit.communitystore.util.FirestoreUtil
import kotlinx.android.synthetic.main.dialog_join_the_fold.view.*
class JoinStoreDialog : AppCompatDialogFragment() {
private lateinit var sharedPreferences: SharedPreferences
private lateinit var listener: EngagedDialogListener
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = context as EngagedDialogListener
} catch (e: ClassCastException) {
throw ClassCastException(
context.toString() +
"must implement ExampleDialogListener"
)
}
}
interface EngagedDialogListener {
fun notThisTime(nope: Boolean)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
sharedPreferences =
androidx.preference.PreferenceManager.getDefaultSharedPreferences(
activity
)
val builder = AlertDialog.Builder(activity)
val inflater = requireActivity().layoutInflater
val view = inflater.inflate(R.layout.dialog_join_the_fold, null)
view.dialog_new_store_name
view.dialog_new_store_email
view.dialog_new_store_password
builder.setView(view)
.setTitle("Thank You for Joining")
.setNegativeButton("Na, Not Now") { dialogInterface, i ->
engagedParticipant(false)
listener.notThisTime(false)
dialogInterface.dismiss()
}
.setPositiveButton("Let's Get Started") { dialogInterface, i ->
engagedParticipant(true)
FirestoreUtil.updateCurrentUser("", null, true)
val intent = Intent(activity, ParticipantLandingPage::class.java)
startActivity(intent)
}
return builder.create()
}
private fun engagedParticipant(engagement: Boolean) {
val editor = sharedPreferences.edit()
editor.putBoolean(AppConstants.ENGAGEMENT, engagement)
editor.apply()
}
}
The Context is your Activity. If you want to reference your parent fragment, you need to use parentFragment (or, on newer versions of Fragments, use requireParentFragment() to get a non-null fragment):
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = requireParentFragment() as EngagedDialogListener
} catch (e: ClassCastException) {
throw ClassCastException(
requireParentFragment().toString() +
"must implement ExampleDialogListener"
)
}
}
I'm trying to get my location using mapbox on an android application built with kotlin. I'm using the locationComponent method to get it, here is my code :
class PlaceholderFragment : Fragment(), OnMapReadyCallback, PermissionsListener {
private var permissionsManager: PermissionsManager = PermissionsManager(this)
private var mapboxMap: MapboxMap? = null
private var myCurrentLocation: LatLng? = null
var navigationMapRoute: NavigationMapRoute? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pageViewModel = ViewModelProviders.of(this).get(PageViewModel::class.java).apply {
setIndex(arguments?.getInt(ARG_SECTION_NUMBER) ?: 1)
}
val theActivity = activity as NavigationActivity?
theSteps = theActivity?.theSteps
case = theActivity?.case
case = theActivity.case
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
activity?.let {
Mapbox.getInstance(
it,
getString(com.innoventiq.arkbeh.R.string.mapbox_access_token2)
)
}
val root =
inflater.inflate(com.innoventiq.arkbeh.R.layout.fragment_navigation, container, false)
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mapView.onCreate(savedInstanceState)
mapView.getMapAsync(this)
getMyLocation()
}
override fun onExplanationNeeded(permissionsToExplain: MutableList<String>?) {
Toast.makeText(
activity,
com.innoventiq.arkbeh.R.string.user_location_permission_explanation,
Toast.LENGTH_LONG
)
.show()
}
override fun onPermissionResult(granted: Boolean) {
if (granted) {
enableLocationComponent(mapboxMap?.style!!)
} else {
Toast.makeText(
activity,
com.innoventiq.arkbeh.R.string.user_location_permission_not_granted,
Toast.LENGTH_LONG
)
.show()
}
}
override fun onMapReady(mapboxMap: MapboxMap) {
this.mapboxMap = mapboxMap
mapboxMap.cameraPosition = CameraPosition.Builder()
.target(myCurrentLocation)
.zoom(14.0)
.build()
//mapView.setOnTouchListener { v, event -> true }
mapboxMap.setStyle(Style.OUTDOORS) {
enableLocationComponent(it)
}
}
#SuppressLint("MissingPermission")
private fun enableLocationComponent(loadedMapStyle: Style) {
// Check if permissions are enabled and if not request
if (PermissionsManager.areLocationPermissionsGranted(activity)) {
// Create and customize the LocationComponent's options
val customLocationComponentOptions = activity?.let {
LocationComponentOptions.builder(it)
.trackingGesturesManagement(true)
.accuracyColor(
ContextCompat.getColor(
activity!!,
com.innoventiq.arkbeh.R.color.colorGreen
)
)
.build()
}
val locationComponentActivationOptions =
activity?.let {
LocationComponentActivationOptions.builder(it, loadedMapStyle)
.locationComponentOptions(customLocationComponentOptions)
.build()
}
// Get an instance of the LocationComponent and then adjust its settings
mapboxMap?.locationComponent?.apply {
// Activate the LocationComponent with options
locationComponentActivationOptions?.let {
this?.activateLocationComponent(
it
)
}
// Enable to make the LocationComponent visible
isLocationComponentEnabled = true
// Set the LocationComponent's camera mode
cameraMode = CameraMode.TRACKING
// Set the LocationComponent's render mode
renderMode = RenderMode.COMPASS
}
} else {
permissionsManager = PermissionsManager(this)
permissionsManager.requestLocationPermissions(activity)
}
}
private fun getMyLocation() {
myCurrentLocation =
mapboxMap?.locationComponent?.lastKnownLocation?.latitude?.let {
mapboxMap!!.locationComponent.lastKnownLocation?.longitude?.let { it1 ->
LatLng(it,
it1
)
}
}
println("the location : $myCurrentLocation ")
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
override fun onResume() {
super.onResume()
mapView.onResume()
}
override fun onPause() {
super.onPause()
mapView.onPause()
}
override fun onStop() {
super.onStop()
mapView.onStop()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}
override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
companion object {
/**
* The fragment argument representing the section number for this
* fragment.
*/
private const val ARG_SECTION_NUMBER = "section_number"
/**
* Returns a new instance of this fragment for the given section
* number.
*/
#JvmStatic
fun newInstance(sectionNumber: Int): PlaceholderFragment {
return PlaceholderFragment().apply {
arguments = Bundle().apply {
putInt(ARG_SECTION_NUMBER, sectionNumber)
}
}
}
}
}
I used almost the full fragment code just trying to give you a clear view of the steps I've used to get the location. When it comes to the line println("the location : $myCurrentLocation ")
inside the getMyLocation() function , it returns this output the location : null ,, any help on this ?
Note
When the map loads, it shows my location perfectly and tracks it, but I just can't get the LatLng of it.
I've found the answer, used LocationComponent.LocationEngine.getLastLocation callBack to get it , here is the code:
private fun getMyLocation() {
mapboxMap?.locationComponent?.locationEngine?.getLastLocation(object :
LocationEngineCallback<LocationEngineResult> {
override fun onSuccess(result: LocationEngineResult?) {
if (result != null) {
myCurrentLocation =
LatLng(result.locations[0].latitude, result.locations[0].longitude)
println("my location is : $myCurrentLocation")
getTheRoute(myCurrentLocation)
}
}
override fun onFailure(exception: Exception) {
toast(getString(R.string.failed_get_location))
}
})
}
While writing an Android app, I encountered a problem with a stuttering animation. I use AHBottomNavigation for navigation, FragNav is for swapping fragments and FlexibleAdapter for RecyclerView.
The application is built from one activity and five fragments. When I try to switch to the first fragment in the application, the BottomNavigation animation freez for a moment. It looks very unsightly. The second time I choose the same fragment, everything works smoothly. It seems to me that it is the fault to initialize the views in the fragment, but I have no idea how to do it differently.
AHBottomNavigation https://github.com/aurelhubert/ahbottomnavigation
FragNav https://github.com/ncapdevi/FragNav
FlexibleAdapter https://github.com/davideas/FlexibleAdapter
Fragment
class GradeFragment : BaseFragment(), GradeView {
#Inject
lateinit var presenter: GradePresenter
private val gradeAdapter = FlexibleAdapter<AbstractFlexibleItem<*>>(null, null, true)
companion object {
fun newInstance() = GradeFragment()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grade, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.run {
attachView(this#GradeFragment)
loadData()
}
}
override fun initView() {
gradeAdapter.run {
isAutoCollapseOnExpand = true
isAutoScrollOnExpand = true
setOnUpdateListener { presenter.onUpdateDataList(it) }
setOnItemClickListener { position ->
getItem(position).let {
if (it is GradeItem) {
GradeDialog.newInstance(it.grade).show(fragmentManager, it.toString())
}
}
}
}
gradeRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = gradeAdapter
}
gradeSwipe.setOnRefreshListener { presenter.loadData(forceRefresh = true) }
}
override fun updateData(data: List<GradeHeader>) {
gradeAdapter.updateDataSet(data, true)
}
override fun isViewEmpty(): Boolean = gradeAdapter.isEmpty
override fun showEmptyView(show: Boolean) {
gradeEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showProgress(show: Boolean) {
gradeProgress.visibility = if (show) VISIBLE else GONE
}
override fun setRefresh(show: Boolean) {
gradeSwipe.isRefreshing = show
}
Presenter
class GradePresenter #Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersManager,
private val gradeRepository: GradeRepository,
private val sessionRepository: SessionRepository) : BasePresenter<GradeView>(errorHandler) {
override fun attachView(view: GradeView) {
super.attachView(view)
view.initView()
}
fun loadData(forceRefresh: Boolean = false) {
disposable.add(sessionRepository.getSemesters()
.map { it.single { semester -> semester.current } }
.flatMap { gradeRepository.getGrades(it, forceRefresh) }
.map { it.groupBy { grade -> grade.subject } }
.map { createGradeItems(it) }
.subscribeOn(schedulers.backgroundThread())
.observeOn(schedulers.mainThread())
.doFinally { view?.setRefresh(false) }
.doOnSuccess { if (it.isEmpty()) view?.showEmptyView(true) }
.doOnError { view?.run { if (isViewEmpty()) showEmptyView(true) } }
.subscribe({ view?.updateData(it) }) { errorHandler.proceed(it) })
}
private fun createGradeItems(items: Map<String, List<Grade>>): List<GradeHeader> {
return items.map {
val gradesAverage = calcAverage(it.value)
GradeHeader().apply {
subject = it.key
average = view?.run {
if (gradesAverage == 0f) emptyAverageString()
else averageString().format(gradesAverage)
}.orEmpty()
number = view?.gradeNumberString(it.value.size).orEmpty()
subItems = (it.value.map { item ->
GradeItem().apply {
grade = item
weightString = view?.weightString().orEmpty()
valueColor = getValueColor(item.value)
}
})
}
}
}
fun onUpdateDataList(size: Int) {
if (size != 0) view?.showProgress(false)
}
After a few days, I managed to solve the problem by updating the SDK to version 28. RecyclerView no longer causes animation jams when inflating