Input validation with MVVM and Data binding

I try to learn the MVVM Architecture by implementing a very simple app that takes three inputs from the user and stores them in a Room Database then display the data in a RecyclerView. From the first try it seems to work well, then the app crashes if one of the inputs is left empty. Now, I want to add some input validations (for now the validations must just check for empty string), but I can’t figure it out. I found many answers on stackoverflow and some libraries that validates the inputs, but I couldn’t integrate those solutions in my app (most probably it is due to my poor implementation of the MVVM). This is the code of my ViewModel:

class MetricPointViewModel(private val repo: MetricPointRepo): ViewModel(), Observable {

    val points = repo.points

    @Bindable
    val inputDesignation = MutableLiveData<String>()

    @Bindable
    val inputX = MutableLiveData<String>()

    @Bindable
    val inputY = MutableLiveData<String>()



    fun addPoint(){
        val id = inputDesignation.value!!.trim()
        val x = inputX.value!!.trim().toFloat()
        val y = inputY.value!!.trim().toFloat()
        insert(MetricPoint(id, x , y))
        inputDesignation.value = null
        inputX.value = null
        inputY.value = null
    }

    private fun insert(point: MetricPoint) = viewModelScope.launch { repo.insert(point) }

    fun update(point: MetricPoint) = viewModelScope.launch { repo.update(point) }

    fun delete(point: MetricPoint) = viewModelScope.launch { repo.delete(point) }

    override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
    }

    override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback?) {
    }
}

and this is the fragment where everything happens:

class FragmentList : Fragment() {
    // TODO: Rename and change types of parameters
    private var param1: String? = null
    private var param2: String? = null

    //Binding object
    private lateinit var binding: FragmentListBinding
    //Reference to the ViewModel
    private lateinit var metricPointVm: MetricPointViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        //Setting up the database
        val metricPointDao = MetricPointDB.getInstance(container!!.context).metricCoordDao
        val repo = MetricPointRepo(metricPointDao)
        val factory = MetricPointViewModelFactory(repo)
        metricPointVm = ViewModelProvider(this, factory).get(MetricPointViewModel::class.java)
        // Inflate the layout for this fragment
        binding = FragmentListBinding.inflate(inflater, container, false)
        binding.apply {
            lifecycleOwner = viewLifecycleOwner
            myViewModel = metricPointVm
        }

        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initRecyclerview()
    }

    private fun displayPoints(){
        metricPointVm.points.observe(viewLifecycleOwner, Observer {
            binding.pointsRecyclerview.adapter = MyRecyclerViewAdapter(it) { selecteItem: MetricPoint -> listItemClicked(selecteItem) }
        })
    }

    private fun initRecyclerview(){
        binding.pointsRecyclerview.layoutManager = LinearLayoutManager(context)
        displayPoints()
    }

    private fun listItemClicked(point: MetricPoint){
        Toast.makeText(context, "Point: ${point._id}", Toast.LENGTH_SHORT).show()
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @param param1 Parameter 1.
         * @param param2 Parameter 2.
         * @return A new instance of fragment FragmentList.
         */
        // TODO: Rename and change types and number of parameters
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            FragmentList().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

I’m planning also to add a long click to the recyclerview and display a context menu in order to delete items from the database. Any help would be appreciated. My recycler view adapter implementation:

class MyRecyclerViewAdapter(private val pointsList: List<MetricPoint>,
                            private val clickListener: (MetricPoint) -> Unit): RecyclerView.Adapter<MyViewHolder>(){
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding: RecyclerviewItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.recyclerview_item, parent, false)
        return MyViewHolder(binding)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(pointsList[position], clickListener)
    }

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

}

class MyViewHolder(private val binding: RecyclerviewItemBinding): RecyclerView.ViewHolder(binding.root){
    fun bind(point: MetricPoint, clickListener: (MetricPoint) -> Unit){
        binding.idTv.text = point._id
        binding.xTv.text = point.x.toString()
        binding.yTv.text = point.y.toString()
        binding.listItemLayout.setOnClickListener{
            clickListener(point)
        }
    }
}

Answer

Try the following,

    fun addPoint(){
        val id = inputDesignation.value!!.trim()
        if(inputX.value == null)
             return

        val x = inputX.value!!.trim().toFloat()

        if(inputY.value == null)
            return

        val y = inputY.value!!.trim().toFloat()
        insert(MetricPoint(id, x , y))
        inputDesignation.value = null
        inputX.value = null
        inputY.value = null
    }

Edit:

you can try the following as well if you wish to let the user know that the value a value is expected

ViewModel

private val _isEmpty = MutableLiveData<Boolean>()
val isEmpty : LiveData<Boolean>
get() = _isEmpty

    fun addPoint(){
        val id = inputDesignation.value!!.trim()
        if(inputX.value == null){
             _isEmpty.value = true
             return
        }

        val x = inputX.value!!.trim().toFloat()

        if(inputY.value == null){
             _isEmpty.value = true
             return
        }

        val y = inputY.value!!.trim().toFloat()
        insert(MetricPoint(id, x , y))
        inputDesignation.value = null
        inputX.value = null
        inputY.value = null
    }

//since showing a error message is an event and not a state, reset it once its done

   fun resetError(){
        _isEmpty.value = null
   }

Fragment Class

metricPointVm.isEmpty.observe(viewLifecycleOwner){ isEmpty ->
    isEmpty?.apply{
         if(it){
              // make a Toast
              metricPointVm.resetError()
         }
    }
}