Android Todo应用:数据库对象更新与跨屏数据传递实现咨询
Todo应用任务更新功能实现方案
我需要为自研Todo应用实现任务更新流程:从任务列表Fragment跳转至任务详情Fragment,点击编辑按钮进入任务编辑Fragment,修改任务标题、描述和日期后,同步更新数据库与RecyclerView,目前不清楚正确的实现方式。
现有代码实现
TaskListFragment
class TaskListFragment : Fragment(R.layout.fragment_task_list) { private lateinit var todoAdapter: TodoAdapter private var _binding: FragmentTaskListBinding? = null private lateinit var todoDao: TodoDao private lateinit var viewModel: TodoViewModel private val binding: FragmentTaskListBinding get() = checkNotNull(_binding) private var todos: MutableList<Todo> = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val appDatabase = AppDatabase.getInstance(this.requireActivity()) todoDao = appDatabase.todoDao() var repository = TaskRepository(todoDao) viewModel = ViewModelProvider(this, TodoViewModelFactory(repository)).get(TodoViewModel::class.java) repository.allTasks.observe(this, Observer { todoList -> todos.clear() todos.addAll(todoList) todoAdapter.notifyDataSetChanged() }) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentTaskListBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) todoAdapter = TodoAdapter(todos,viewModel) binding.rvTodoItems.adapter = todoAdapter binding.rvTodoItems.layoutManager = LinearLayoutManager(this.activity) binding.btnAddTodo.setOnClickListener { NewTaskSheet().show(requireActivity().supportFragmentManager, "NewTaskTag") } requireActivity().supportFragmentManager.setFragmentResultListener( "requestKey", this ) { requestKey, bundle -> val newTodo = bundle.getSerializable("bundleKey") as Todo GlobalScope.launch { viewModel.insert(newTodo) } todoAdapter.notifyDataSetChanged() } binding.rvTodoItems.addItemDecoration( DividerItemDecoration( binding.rvTodoItems.getContext(), DividerItemDecoration.VERTICAL ) ) } override fun onDestroyView() { super.onDestroyView() _binding = null } }
TodoAdapter
class TodoAdapter( private var todos: MutableList<Todo>, private val viewModel: TodoViewModel ):RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() { class TodoViewHolder( ItemTodoBinding: ItemTodoBinding): RecyclerView.ViewHolder(ItemTodoBinding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val ItemTodoBinding = ItemTodoBinding.inflate(layoutInflater, parent, false) return TodoViewHolder( ItemTodoBinding ) } private fun toggleStrikeThrough(tvTodoTitle: TextView, isChecked:Boolean){ if(isChecked){ tvTodoTitle.paintFlags = tvTodoTitle.paintFlags or STRIKE_THRU_TEXT_FLAG }else{ tvTodoTitle.paintFlags = tvTodoTitle.paintFlags and STRIKE_THRU_TEXT_FLAG.inv() } } override fun onBindViewHolder(holder: TodoViewHolder, position: Int) { val curTodo = todos[position] holder.itemView.apply{ val tvTodoTitle = findViewById(R.id.tvTodoTitle) as TextView tvTodoTitle.text = curTodo.title val cbDone = findViewById(R.id.cbDone) as CheckBox val deleteBtn = findViewById(R.id.deleteBtn) as Button cbDone.isChecked = curTodo.isChecked toggleStrikeThrough(tvTodoTitle, curTodo.isChecked) cbDone.setOnCheckedChangeListener { _, isChecked -> toggleStrikeThrough(tvTodoTitle, isChecked) curTodo.isChecked = !curTodo.isChecked } cbDone.setOnClickListener(View.OnClickListener{ GlobalScope.launch { viewModel.update(curTodo) viewModel.delete(curTodo) } todos.removeAll { todo -> todo.isChecked } notifyDataSetChanged() }) deleteBtn.setOnClickListener{ val alertDialogBuilder = AlertDialog.Builder(this.context) alertDialogBuilder.setMessage("Do you want to delete this task?") alertDialogBuilder.setPositiveButton("Yes"){ dialogInterface, which -> Toast.makeText(context,"Delete",Toast.LENGTH_LONG).show() GlobalScope.launch { viewModel.delete(curTodo) } } alertDialogBuilder.setNegativeButton("No"){ dialogInterface, which -> Toast.makeText(context,"Cancelled",Toast.LENGTH_LONG).show() } val alertDialog: AlertDialog = alertDialogBuilder.create() alertDialog.setCancelable(false) alertDialog.show() notifyDataSetChanged() } tvTodoTitle.setOnClickListener { val request = NavDeepLinkRequest.Builder .fromUri("${TASK_DETAIL_URI}?${TASK_BUNDLE_KEY}=${curTodo.title}&${TASK_DESCR_BUNDLE_KEY}=${curTodo.description}&${TASK_DATE_BUNDLE_KEY}=${curTodo.date}".toUri()) .build() Log.d("MyApp", curTodo.description) findNavController().navigate(request) } } } override fun getItemCount(): Int { return todos.size } companion object{ const val TASK_DETAIL_URI = "app://taskmaster.taskDetailFragment" const val TASK_LIST_URI ="app://taskmaster.taskListFragment" const val TASK_EDIT_URI="app://taskmaster.taskEditFragment" } }
TaskDetailFragment
class TaskDetailFragment : Fragment() { private var _binding: FragmentTaskDetailBinding? = null private val binding: FragmentTaskDetailBinding get() = checkNotNull(_binding) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentTaskDetailBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val title = arguments?.getString(TASK_BUNDLE_KEY) val date = arguments?.getString(TASK_DATE_BUNDLE_KEY) val descr = arguments?.getString(TASK_DESCR_BUNDLE_KEY) Log.d("MyApp", descr.toString()) binding.taskTitle.text = title binding.selectedDate.text = date binding.selectedDescription.text = descr binding.btnBack.setOnClickListener { val request = NavDeepLinkRequest.Builder .fromUri(TASK_LIST_URI.toUri()) .build() findNavController().navigate(request) } binding.btnEdit.setOnClickListener { val request = NavDeepLinkRequest.Builder .fromUri("${TodoAdapter.TASK_EDIT_URI}?${TASK_BUNDLE_KEY}=${title}&${TASK_DESCR_BUNDLE_KEY}=${descr}&${TASK_DATE_BUNDLE_KEY}=${date}".toUri()) .build() findNavController().navigate(request) } } }
TaskEditFragment
class TaskEditFragment : Fragment() { private var _binding: FragmentTaskEditBinding? = null private val binding: FragmentTaskEditBinding get() = checkNotNull(_binding) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentTaskEditBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val title = arguments?.getString(Constants.TASK_BUNDLE_KEY) val date = arguments?.getString(Constants.TASK_DATE_BUNDLE_KEY) val descr = arguments?.getString(Constants.TASK_DESCR_BUNDLE_KEY) binding.title.setText(title) binding.date.setText(date) binding.description.setText(descr) binding.btnSave.setOnClickListener { val request = NavDeepLinkRequest.Builder .fromUri("${TodoAdapter.TASK_DETAIL_URI}?${Constants.TASK_EDIT_BUNDLE_KEY}=${binding.title.text}&${Constants.TASK_EDIT_DESCR_BUNDLE_KEY}=${binding.description.text}&${Constants.TASK_EDIT_DATE_BUNDLE_KEY}=${binding.date.text}".toUri()) .build() findNavController().navigate(request) } } }
TodoViewModel
class TodoViewModel(private val repository: TaskRepository): ViewModel() { fun insert(todo: Todo) = viewModelScope.launch { repository.insert(todo) } fun update(todo: Todo) = viewModelScope.launch{ repository.update(todo) } fun delete(todo: Todo) = viewModelScope.launch{ repository.delete(todo) } } class TodoViewModelFactory(private val repository: TaskRepository) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { if (modelClass.isAssignableFrom(TodoViewModel::class.java)) { @Suppress("UNCHECKED_CAST") return TodoViewModel(repository) as T } throw IllegalArgumentException("Unknown ViewModel class") } }
TaskRepository
class TaskRepository(private val todoDao: TodoDao) { val allTasks: LiveData<List<Todo>> = todoDao.getAllTasks() @Suppress("RedundantSuspendModifier") @WorkerThread suspend fun insert(todo: Todo) { todoDao.insert(todo) } suspend fun update(todo: Todo){ todoDao.update(todo) } suspend fun delete(todo: Todo){ todoDao.delete(todo) } }
导航图(task_graph.xml)
<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/task_graph" app:startDestination="@id/taskListFragment"> <fragment android:id="@+id/taskListFragment" android:name="com.bignerdranch.android.taskmaster.TaskListFragment" android:label="TaskListFragment"> <deepLink android:id="@+id/deepLink2" app:uri="app://taskmaster.taskListFragment" /> </fragment> <fragment android:id="@+id/taskDetailFragment" android:name="com.bignerdranch.android.taskmaster.TaskDetailFragment" android:label="TaskDetailFragment"> <deepLink android:id="@+id/deepLink" app:uri="app://taskmaster.taskDetailFragment?taskBundleKey={taskBundleKey}&taskDateBundleKey={taskDateBundleKey}&taskDescrBundleKey={taskDescrBundleKey}"/> <argument android:name="taskBundleKey" app:argType="string"/> <argument android:name="taskDateBundleKey" app:argType="string"/> <argument android:name="taskDescrBundleKey" app:argType="string"/> </fragment> <fragment android:id="@+id/taskEditFragment" android:name="com.bignerdranch.android.taskmaster.TaskEditFragment" android:label="TaskEditFragment"> <deepLink android:id="@+id/deepLink3" app:uri="app://taskmaster.taskEditFragment?taskBundleKey={taskBundleKey}&taskDateBundleKey={taskDateBundleKey}&taskDescrBundleKey={taskDescrBundleKey}" /> <argument android:name="taskBundleKey" app:argType="string"/> <argument android:name="taskDateBundleKey" app:argType="string"/> <argument android:name="taskDescrBundleKey" app:argType="string"/> </fragment> </navigation>
任务更新功能修复与实现步骤
1. 传递任务ID而非单独字段
当前代码通过传递任务的标题、描述等字段跳转,无法唯一标识任务(可能存在同名任务),应改为传递任务的ID:
- 修改
Todo实体类,确保有唯一ID字段(通常用@PrimaryKey(autoGenerate = true)注解) - 更新导航图,将参数改为
taskId(类型为long或int):
<!-- TaskDetailFragment参数 --> <argument android:name="taskId" app:argType="long"/> <!-- TaskEditFragment参数 --> <argument android:name="taskId" app:argType="long"/>
- 修改TodoAdapter中跳转代码,传递taskId:
tvTodoTitle.setOnClickListener { val request = NavDeepLinkRequest.Builder .fromUri("${TASK_DETAIL_URI}?taskId=${curTodo.id}".toUri()) .build() findNavController().navigate(request) }
2. 详情页通过ID加载任务数据
TaskDetailFragment不再直接用参数显示数据,而是通过ViewModel根据ID加载任务:
class TaskDetailFragment : Fragment() { private lateinit var viewModel: TodoViewModel private var taskId: Long = -1 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) taskId = arguments?.getLong("taskId") ?: -1 val repository = TaskRepository(AppDatabase.getInstance(requireActivity()).todoDao()) viewModel = ViewModelProvider(this, TodoViewModelFactory(repository)).get(TodoViewModel::class.java) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.getTaskById(taskId).observe(viewLifecycleOwner) { todo -> todo?.let { binding.taskTitle.text = it.title binding.selectedDate.text = it.date binding.selectedDescription.text = it.description } } binding.btnEdit.setOnClickListener { val request = NavDeepLinkRequest.Builder .fromUri("${TodoAdapter.TASK_EDIT_URI}?taskId=${taskId}".toUri()) .build() findNavController().navigate(request) } } }
同时在ViewModel、Repository和Dao中添加根据ID获取任务的方法:
// TodoViewModel fun getTaskById(id: Long) = repository.getTaskById(id) // TaskRepository fun getTaskById(id: Long): LiveData<Todo?> = todoDao.getTaskById(id) // TodoDao @Query("SELECT * FROM todo WHERE id = :id") fun getTaskById(id: Long): LiveData<Todo?>
3. 编辑页更新数据库并返回
TaskEditFragment根据ID加载任务,修改后调用ViewModel更新数据库,完成后返回列表页:
class TaskEditFragment : Fragment() { private lateinit var viewModel: TodoViewModel private var taskId: Long = -1 private var originalTodo: Todo? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) taskId = arguments?.getLong("taskId") ?: -1 val repository = TaskRepository(AppDatabase.getInstance(requireActivity()).todoDao()) viewModel = ViewModelProvider(this, TodoViewModelFactory(repository)).get(TodoViewModel::class.java) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.getTaskById(taskId).observe(viewLifecycleOwner) { todo -> todo?.let { originalTodo = it binding.title.setText(it.title) binding.date.setText(it.date) binding.description.setText(it.description) } } binding.btnSave.setOnClickListener { originalTodo?.let { todo -> val updatedTodo = todo.copy( title = binding.title.text.toString(), description = binding.description.text.toString(), date = binding.date.text.toString() ) viewModel.update(updatedTodo) findNavController().popBackStack(R.id.taskListFragment, false) } } } }
4. 利用LiveData自动更新RecyclerView
当前TaskListFragment已经观察了repository.allTasks,只要数据库更新,LiveData会自动通知,此时只需要确保Adapter的数据源和LiveData同步,无需手动调用notifyDataSetChanged():
// TaskListFragment中修改LiveData观察逻辑 repository.allTasks.observe(viewLifecycleOwner) { todoList -> todos.clear() todos.addAll(todoList) todoAdapter.notifyDataSetChanged() }
注意:使用viewLifecycleOwner而非this,避免内存泄漏。
5. 修复现有代码中的其他问题
- 删除
GlobalScope的使用,改用viewModelScope处理异步操作,避免内存泄漏 - Todo
相关产品推荐
相关产品推荐

