Я пытаюсь написать приложение, используя различные архитектурные компоненты/концепции, и сейчас я не уверен, какое лучшее/предпочтительное решение одной из моих проблем.
У меня есть фрагмент внутри приложения, которое имеет RecyclerView и отображает элементы из базы данных в списке. У каждого элемента есть кнопки удаления и редактирования. Теперь мне нужно реализовать эти функции, но я немного не понимаю, как это сделать. Я могу либо обновить базу данных и прослушать изменения, чтобы обновить RecyclerView, либо я могу сначала обновить RecyclerView и обновить базу данных, когда фрагмент уничтожен/приостановлен. Но я изо всех сил пытаюсь реализовать любой из этих подходов.
Представление необходимо обновлять, когда элемент был изменен (отображен новый заголовок и т. д.) или удален из списка. Если я помещу onClickListener внутрь ViewHolder, у меня не будет доступа ко всему набору данных. Если я помещу его внутрь onCreateViewHolder, у меня не будет доступа к элементу, который был нажат. А в OnBindViewHolder у меня нет доступа к кнопке.
В onClickListener мне нужно либо изменить данные, отображаемые RecycleViewer (и обновить базу данных, когда фрагмент уничтожен/приостановлен), либо напрямую вызвать метод в ViewModel, который обращается к базе данных. Я также не знаю, как получить доступ к ViewModel и его методам из адаптера (может быть, это вообще не следует делать), поскольку ViewModelProvider(?)[PlacesViewModel::class.java] нуждается в ViewModelStoreOwner или ViewModelStore, и я не не очень понимаю ни один из них, ни как получить к ним доступ из адаптера.
Что мне не хватает? Каков наилучший/правильный способ сделать это? Это может быть что-то очень очевидное, но среди всех этих новых концепций я чувствую себя немного потерянным. Как я уже сказал, я новичок в Android и только начал использовать большинство понятий здесь (LiveData, ViewModel и т. д.). Возможно, я не использую их должным образом, и весь подход неверен.
Вот фрагмент:
class PlacesFragment : Fragment() {
private var _binding: FragmentPlacesBinding? = null
private val binding get(): FragmentPlacesBinding = _binding!!
private lateinit var mViewModel: PlacesViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mViewModel = ViewModelProvider(this)[PlacesViewModel::class.java]
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentPlacesBinding.inflate(inflater, container, false)
// set the adapter for the recyclerview to display the list items
val recyclerView = _binding!!.recyclerViewPlaces
mViewModel.places.observe(viewLifecycleOwner){ places ->
recyclerView.adapter = RecyclerViewPlacesAdapter(places)
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
ViewModel:
class PlacesViewModel(app: Application): AndroidViewModel(app) {
private var _places: MutableLiveData<MutableList<Place>> = MutableLiveData()
val places get() = _places
init {
viewModelScope.launch {
try{
val db = AppDatabase.getInstance(getApplication())
_places.value = db.placeDao().getAll().toMutableList()
} catch(e: Exception){
Log.e("Error", e.stackTraceToString())
}
}
}
fun deletePlace(place: Place){
viewModelScope.launch {
try{
val db = AppDatabase.getInstance(getApplication())
_places.value?.remove(place)
//db.placeDao().delete(place)
} catch(e: Exception){
Log.e("Error", e.stackTraceToString())
}
}
}
}
и адаптер:
class RecyclerViewPlacesAdapter(private val dataSet: List<Place>) :
RecyclerView.Adapter<RecyclerViewPlacesAdapter.ViewHolder>() {
class ViewHolder(itemBinding: ListTilePlacesBinding) : RecyclerView.ViewHolder(itemBinding.root) {
private val placeTitle: TextView = itemBinding.placeTitle
private val placeAddress: TextView = itemBinding.placeAddress
var place: Place? = null
fun setValues(){
if (place != null){
placeTitle.text = place!!.title
placeAddress.text = place!!.address //TODO either address or lat/long
}
}
init {
// Define click listener for the ViewHolder's View.
itemBinding.buttonDelete.setOnClickListener {
// TODO remove item from dataset and in RecyclerView
}
itemBinding.buttonEdit.setOnClickListener {
// TODO edit item and display changes in RecyclerView
}
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
val itemBinding = ListTilePlacesBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(itemBinding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.place = dataSet[position]
holder.setValues()
}
override fun getItemCount() = dataSet.size
}
Обновлено: Как предложил @cactustictacs, я добавил в адаптер метод deleteItem(), который удалял элемент из набора данных и вызывал notifyItemRemoved для обновления представления. Я также передал ViewModel адаптеру для вызова его метода. Раньше я не был уверен, что это правильный подход, чтобы просто передать его другому методу.
class PlaceListener (val clickListener: (p: Place) -> Unit) {
fun onClick (p: Place) = clickListener (p)
}
class RecyclerViewPlacesAdapter (
private val deleteClickListener: PlaceListener,
private val updateClickListener: PlaceListener,
): ListAdapter <Place, RecyclerViewPlacesAdapter.ViewHolder> (PlaceDiffCallback ()) {
override fun onCreateViewHolder (parent: ViewGroup, viewType: Int): RecyclerViewPlacesAdapter.ViewHolder {
return RecyclerViewPlacesAdapter.ViewHolder.from (parent)
}
override fun onBindViewHolder (holder: RecyclerViewPlacesAdapter.ViewHolder, position: Int) {
val item = getItem (position)
return holder.bind (item, deleteClickListener, updateClickListener)
}
class ViewHolder private constructor (private val binding: ListTilePlacesBinding):
RecyclerView.ViewHolder (binding.root) {
fun bind (item: Place, deleteliCkListener: PlaceListener, updateClickListener: PlaceListener) {
binding.apply {
buttonDelete.setOnClickListener {
deleteClickListener.onClick(item)
}
buttonEdit.setOnClickListener {
updateClickListener.onClick(item)
}
}
}
companion object {
fun from (parent: ViewGroup): RecyclerViewPlacesAdapter.ViewHolder {
val layoutInflater = LayoutInflater.from (parent.context)
val binding = ListTilePlacesBinding.inflate (layoutInflater, parent, false)
return RecyclerViewPlacesAdapter.ViewHolder (binding)
}
}
}
class PlaceDiffCallback: DiffUtil.ItemCallback <Place> () {
override fun areItemsTheSame (oldItem: Place, newItem: Place): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame (oldItem: Place, newItem: Place): Boolean {
// example of control of 3 properties
return (
oldItem.property1 == newItem.property1 &&
oldItem.property2 == newItem.property2 &&
oldItem.property3 == newItem.property3
)
}
}
}
private lateinit var adapter: RecyclerViewPlacesAdapter
// after you initialize the binding
adapter = RecyclerViewPlacesAdapter (
PlaceListener {place -> mViewModel.deletePlace (place)},
PlaceListener {place -> mViewModel.updatePlace (place)},
)
val recyclerView = _binding!!.recyclerViewPlaces
recyclerView.adapter = adapter
mViewModel.places.observe (viewLifecycleOwner) {places ->
adapter.submitList (places)
adapter.notifyDataSetChanged ()
}
Если вам не нравится использовать adapter.submitList(places) и adapter.notifyDataSetChanged(), вы всегда можете передать им список в конструкторе (в этом случае в конструкторе адаптера у вас будет список объектов Place и 2 объекта placeClickListener )
есть 2 случая: 1) элемент все еще присутствует в списке (вы видите это, поставив точку останова, когда смотрите на список) 2) вам не хватает строки 'adapter.notifyDataSetChanged()' после отправки с новым линия
P.S. Если мой ответ помог вам, не забудьте отметить его как «полезный», нажав на стрелку выше.
В широком смысле идея состоит в том, что ваш ViewModel
— это то, с чем общается слой пользовательского интерфейса (представление). Он предоставляет состояние данных (например, через объект LiveData
), которое ваш пользовательский интерфейс observe
s, и это то, что управляет тем, что отображает пользовательский интерфейс. Наблюдатель получает новые данные -> вы их отображаете.
Пользовательский интерфейс также уведомляет виртуальную машину о взаимодействиях с пользователем — нажатиях кнопок и т.п. Итак, в вашем случае у вас есть функция deletePlace
, которая в основном говорит виртуальной машине: «Эй, пользователь только что решил удалить это, делай то, что тебе нужно». Поскольку виртуальная машина представляет текущее состояние, это то, что вам нужно обновлять всякий раз, когда что-либо происходит (и она внутренне обрабатывает такие вещи, как сохранение этого состояния).
Это два совершенно разных пути, изолированных друг от друга. Помните, что пользовательский интерфейс отображает что виртуальная машина говорит ему, поэтому ему не нужно реагировать на нажатие пользователем кнопки удаления. Это просто перенаправляется на виртуальную машину. Если виртуальная машина решает, что состояние изменилось обновит свой LiveData
, пользовательский интерфейс будет отслеживать новые данные, а потом обновит. Вы в основном рассматриваете виртуальную машину как источник правды и храните логику обновления вне уровня представления.
Это значительно упрощает работу, потому что, когда вашему пользовательскому интерфейсу просто нужно наблюдать за некоторым состоянием виртуальной машины, каждое обновление будет одинаковым. Фрагмент загружен, заполнение списка? Наблюдайте за LiveData
, получайте результат, покажите его. Приложение восстановлено из фона, возможно, с уничтоженным процессом? Наблюдать за вещью, отображать всякий раз, когда приходит обновление. Пользователь удаляет вещь или обновляет ее? Вы наблюдаете за данными, когда изменение сохраняется, оно обновляется. И т.д! Каждый раз одна и та же логика. Не нужно беспокоиться о сохранении состояния при уничтожении фрагмента — все состояние находится в ВМ!
(Из этого могут быть некоторые исключения, например, если удаление требует сетевого вызова, может занять некоторое время, но вы хотите, чтобы изменение произошло немедленно в представлении. Но опять же, это может быть просто деталь реализации в виртуальной машине - это может обновить свое локальное состояние данных и позволить удаленному обновлению происходить в фоновом режиме)
Что касается реализации, лично мне кажется, что чище, чтобы ViewHolder
вызывал метод в Adapter
, например deleteItem(index: Int)
или что-то еще — просто потому, что VH — это компонент пользовательского интерфейса, а адаптер — это то, что находится между набором данных и пользовательский интерфейс, предназначенный для его отображения. Больше похоже на то, что работа адаптера заключается в обработке «пользователь щелкнул удалить этот элемент» и т. д., Если это имеет смысл. Хотя это не имеет большого значения.
И типично делать Adapter
inner class
своего родителя Fragment
- таким образом, если фрагмент имеет ссылку на модель представления, адаптер может просто ее увидеть. В противном случае вы можете просто передать/установить его на адаптере в качестве свойства во время установки. ViewModel
— это общий ресурс, поэтому не беспокойтесь о его передаче.
Спасибо! Я отредактировал свой пост выше и объяснил, что я изменил. В конце концов это было не очень сложно, но я не был уверен, что это действительно правильный / предполагаемый подход. Я не был уверен, смогу ли я просто передать модель просмотра адаптеру вот так. Теперь я понял, что не имеет значения, получу ли я доступ к нему через viewmodelprovider во фрагменте и передам адаптеру (что я сейчас и делаю) или попытаюсь использовать viewmodelprovider в адаптере (что я не смог не пойму как сделать)
@Wckd_Panda да, пусть фрагмент обрабатывает получение виртуальной машины и передает ее адаптеру (или просто делает ее видимой для адаптера) - все это часть вашей первоначальной настройки. Решение, которое вы делаете, заключается в сохранении двух наборов состояний: одного на уровне виртуальной машины и одного в пользовательском интерфейсе (изменение внутреннего набора данных адаптера). Это сложнее, потому что вам нужно поддерживать оба и обновлять пользовательский интерфейс отдельно для разных ситуаций (например, удаление кликов и просмотр новых данных виртуальной машины).
@Wckd_Panda то, что у вас есть сейчас, должен, все в порядке (и это был старый способ ведения дел), но он не использует модель представления. И, например, если вместо этого вы начнете использовать эту строку DAO, то, как вы ее сейчас настроили, означает, что внутренние данные виртуальной машины (в LiveData
) не будут обновляться, чтобы отражать какие-либо изменения в базе данных (вы просто получаете перечислить один раз). Это означает, что данные виртуальной машины и данные адаптера не синхронизированы, и если вы потеряете данные пользовательского интерфейса (скажем, действие будет уничтожено в фоновом режиме), они захватят устаревшие данные виртуальной машины (которые не будут извлекаться из БД до тех пор, пока приложение перезапускается)
@Wckd_Panda, в основном, подход «выполнять действия на виртуальной машине, наблюдать за ее данными и отображать любые обновления», который я изложил, заключается в том, как мы должны использовать всю эту настройку, и он был добавлен, чтобы упростить настройку и обдумывание ( и выявлять ошибки). В том, как работает Android, есть много ошибок: пользовательский интерфейс напрямую общается с виртуальной машиной и отображает любое ее состояние. На ваше усмотрение, конечно! Но я бы порекомендовал использовать этот подход — это рекомендуемая передовая практика в Jetpack: developer.android.com/jetpack/guide#recommended-app-arch
Он вызывает метод ViewModel, и это здорово. Спасибо тебе за это! Но он не удаляет элемент из представления. Что-то еще отсутствует? Я думал, что если я изменю список внутри MutableLiveData в ViewModel (заменю, удалю или добавлю элемент), наблюдатель уведомит и обновит представление, но, похоже, это не так.