Проверка ввода с помощью MVVM и привязки данных

#android #validation #kotlin #input #mvvm

#Android #проверка #kotlin #ввод #mvvm

Вопрос:

Я пытаюсь изучить архитектуру MVVM, реализуя очень простое приложение, которое принимает три входных данных от пользователя и сохраняет их в базе данных Room, а затем отображает данные в RecyclerView. С первой попытки кажется, что все работает хорошо, затем приложение вылетает, если один из входных данных остается пустым. Теперь я хочу добавить некоторые проверки ввода (на данный момент проверки должны просто проверять наличие пустой строки), но я не могу понять это. Я нашел много ответов на stackoverflow и некоторые библиотеки, которые проверяют входные данные, но я не смог интегрировать эти решения в свое приложение (скорее всего, это связано с моей плохой реализацией MVVM). Это код моей 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?) {
    }
}
 

и это фрагмент, в котором все происходит:

 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)
                }
            }
    }
}
 

Я планирую также добавить длинный щелчок в recyclerview и отобразить контекстное меню, чтобы удалить элементы из базы данных. Любая помощь будет оценена.
Моя реализация адаптера представления recycler:

 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)
        }
    }
}
 

Ответ №1:

Попробуйте следующее,

     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
    }
 

Редактировать:

вы также можете попробовать следующее, если хотите сообщить пользователю, что ожидается значение a value

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
   }
 

Класс фрагмента

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

Комментарии:

1. Спасибо @Sekiro, простое, но эффективное решение.

2. @abadil проверьте отредактированный ответ, если вы хотите сообщить пользователю ожидаемое значение, и если это решит вашу проблему, примите ответ, счастливого кодирования

3. еще раз спасибо @Sekiro, все работает отлично.