androidsqlitekotlinandroid-safe-args

NavArgs returning a RuntimeException


NavArgs not returning a value, I just get this when I try to update through the setOnClickListener in update fragment:

java.lang.RuntimeException: java.lang.reflect.InvocationTargetException



Caused by: java.lang.IllegalArgumentException: Required argument "currentAlarm" is missing and does not have an android:defaultValue

I need to return the ID from the alarm selected in the RecyclerView, so a default value wouldn't make sense here.

The NavArgs are set in the nav-view correctly, with no default. Could the model and ID be set up wrong with autoGenerate? or is going via the adapter the problem here?

update fragment:

class UpdateFragment : Fragment() {

private val timePickerUtil = TimePickerUtil()
lateinit var binding: FragmentUpdateBinding
private lateinit var alarmViewModel: AlarmViewModel
private val args by navArgs<UpdateFragmentArgs>()

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    binding = FragmentUpdateBinding.inflate(inflater, container, false)

    alarmViewModel = ViewModelProvider(this)[AlarmViewModel::class.java]

    binding.fragmentBtnUpdateAlarm.setOnClickListener {
        updateAlarm()
        Navigation.findNavController(requireView())
            .navigate(R.id.action_updateFragment_to_homeFragment)
    }
    return binding.root
}

private fun updateDatabase(id: Int, hour: Int, minute: Int, repeat: Boolean) {
    val alarm = Alarm(id, hour, minute, repeat)
    alarmViewModel.update(alarm)
}

private fun updateAlarm() {
    val timePicker = binding.fragmentUpdateAlarmTimePicker
    val id = args.currentAlarm.id
    val hour = timePickerUtil.getTimePickerHour(timePicker)
    val minute = timePickerUtil.getTimePickerMinute(timePicker)
    val repeat = binding.fragmentUpdateAlarmRecurring.isChecked

    val alarmManager = AlarmManager(
        id,
        hour,
        minute,
        true,
        binding.fragmentUpdateAlarmRecurring.isChecked
    )

    updateDatabase(id, hour, minute, repeat)
    alarmManager.cancel(requireContext())
    alarmManager.schedule(requireContext())
}

}

adapter:

class AlarmListAdapter() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {

class MyViewHolder(binding: LayoutAlarmBinding) : RecyclerView.ViewHolder(binding.root)

private var alarmList = ArrayList<Alarm>()


private var onItemClickListener: OnItemClickListener? = null

interface OnItemClickListener{
    fun onClick(alarm: Alarm)
    fun onLongClick(alarm: Alarm)
}

fun setOnItemClickListener(listener: OnItemClickListener){
    onItemClickListener = listener
}


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val binding = LayoutAlarmBinding.inflate(LayoutInflater.from(parent.context))
    return MyViewHolder(binding)
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    val currentItem = alarmList[position]
    val minute = currentItem.minute
    holder.itemView.findViewById<TextView>(R.id.tv_alarm_time).text =
        if (minute >= 10) {
            "${currentItem.hour}:${currentItem.minute}"
        } else {
            "${currentItem.hour}:0${currentItem.minute}"
        }

    holder.itemView.setOnClickListener{
        if(onItemClickListener != null){
            onItemClickListener?.onClick(currentItem)
        }
    }

    holder.itemView.setOnLongClickListener {
        if(onItemClickListener != null){
            onItemClickListener?.onLongClick(currentItem)
        }
        true
    }
}

override fun getItemCount(): Int {
    return alarmList.size
}

fun setData(alarm: List<Alarm>) {
    alarmList.clear()
    alarmList.addAll(alarm)
    notifyDataSetChanged()
}

}

HomeFragment:

class HomeFragment : Fragment() {

lateinit var binding: FragmentHomeBinding
private lateinit var alarmViewModel: AlarmViewModel

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    binding = FragmentHomeBinding.inflate(inflater, container, false)

    // RecyclerView
    val adapter = AlarmListAdapter()
    val recyclerView = binding.recyclerView
    recyclerView.adapter = adapter
    recyclerView.layoutManager = LinearLayoutManager(requireContext())

    //ViewModel
    alarmViewModel = ViewModelProvider(this).get(AlarmViewModel::class.java)
    alarmViewModel.readAlarmData.observe(viewLifecycleOwner, Observer { alarm ->
        adapter.setData(alarm)
    })

    binding.btnAddAlarm.setOnClickListener {
        Navigation.findNavController(requireView())
            .navigate(R.id.action_homeFragment_to_newAlarmFragment)
    }

    adapter.setOnItemClickListener(object : AlarmListAdapter.OnItemClickListener {

        override fun onClick(alarm: Alarm) {
            Navigation.findNavController(requireView())
                .navigate(R.id.action_homeFragment_to_updateFragment)
        }

        override fun onLongClick(alarm: Alarm) {
            val deleteBuilder = AlertDialog.Builder(requireContext())
            deleteBuilder.setPositiveButton("Delete") { _, _ ->
                alarmViewModel.delete(alarm)
                Toast.makeText(context, "Alarm Deleted", Toast.LENGTH_SHORT)
                    .show()
            }
            deleteBuilder.setNegativeButton("Cancel") { _, _ ->

            }
            deleteBuilder.setTitle("Delete Alarm?")
            deleteBuilder.create().show()
        }
    })

    return binding.root
}

}

and my alarm model:

@Parcelize
@Entity(tableName = "alarm_table")
data class Alarm(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val hour: Int,
    val minute: Int,
    val repeat: Boolean
): Parcelable

Solution

  • Safe args creates its own classes (like here HomeFragmentDirections). So in homeFragment, where you are setting up onclick listeners for the adapter, you should use->

    adapter.setOnItemClickListener(object : AlarmListAdapter.OnItemClickListener {
    
            override fun onClick(alarm: Alarm) {
                Navigation.findNavController(requireView())
                    .navigate(HomeFragmentDirections.actionHomeFragmentToUpdateFragment(params))
            }
    
            override fun onLongClick(alarm: Alarm) {
                // preform longclick action here
            }
        })
    
    

    And add a param (in your case, id may be of type int or String) of the required type in UpdateFragment in the corresponding navgraph.

    I hope you got my point and if not don't hesitate to comment. You can read about this on the android developer website here.