使用DeepLink在Fragment间传数据遇空值错误的无ViewModel解决方案
问题描述
点击任务标题打开详情页时提示“所需值为空”,需传递任务标题、日期、描述数据,采用data class实现,不使用ViewModel,寻求解决方案。
相关代码
TodoAdapter
package com.bignerdranch.android.taskmaster import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.CheckBox import android.widget.TextView import androidx.core.net.toUri import androidx.navigation.NavDeepLinkRequest import androidx.navigation.findNavController import androidx.navigation.fragment.NavHostFragment import androidx.recyclerview.widget.RecyclerView import com.bignerdranch.android.taskmaster.Constants.TASK_BUNDLE_KEY import com.bignerdranch.android.taskmaster.Constants.TASK_DATE_BUNDLE_KEY import com.bignerdranch.android.taskmaster.TaskListFragment.Companion.TASK_DETAIL_URI import com.bignerdranch.android.taskmaster.databinding.ItemTodoBinding import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch class TodoAdapter( private var todos: MutableList<Todo>, private val todoDao: TodoDao ):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( 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 { todoDao.update(curTodo) todoDao.delete(curTodo) } todos.removeAll { todo -> todo.isChecked } notifyDataSetChanged() }) deleteBtn.setOnClickListener{ GlobalScope.launch { todoDao.delete(curTodo) } notifyDataSetChanged() } tvTodoTitle.setOnClickListener { val request = NavDeepLinkRequest.Builder .fromUri("${TASK_DETAIL_URI}?${TASK_BUNDLE_KEY}=${curTodo.title}&${TASK_DATE_BUNDLE_KEY}=${curTodo.date}&${TASK_DESCR_BUNDLE_KEY}=${curTodo.description}".toUri()) .build() findNavController().navigate(request) } } } override fun getItemCount(): Int { return todos.size } }
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 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() todoDao.getAllTasks().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, todoDao) 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 { todoDao.insert(newTodo) } todoAdapter.notifyDataSetChanged() } binding.rvTodoItems.addItemDecoration( DividerItemDecoration( binding.rvTodoItems.getContext(), DividerItemDecoration.VERTICAL ) ) } }
TaskDetailFragment
class TaskDetailFragment : Fragment() { private var _binding: FragmentTaskDetailBinding? = null private val binding: FragmentTaskDetailBinding get() = checkNotNull(_binding) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val title = arguments?.getString(TASK_BUNDLE_KEY) val date = arguments?.getString(TASK_DATE_BUNDLE_KEY) val descr = arguments?.getString(TASK_DESCR_BUNDLE_KEY) binding.taskTitle.setText(title.toString()) binding.selectedDate.setText(date.toString()) binding.selectedDescription.setText(descr.toString()) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = FragmentTaskDetailBinding.inflate(inflater, container, false) return binding.root } }
Constants
object Constants { const val TASK_BUNDLE_KEY = "taskBundleKey" const val TASK_DATE_BUNDLE_KEY = "taskDateBundleKey" const val TASK_DESCR_BUNDLE_KEY = "taskDescrBundleKey" }
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" /> <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}" /> <argument android:name="taskBundleKey" app:argType="string" /> <argument android:name="taskDateBundleKey" app:argType="string" /> </fragment> <deepLink android:id="@+id/deepLink2" app:uri="app://taskmaster.taskDetailFragment" /> </navigation>
解决方案
1. 修复导航图参数配置
导航图中缺少描述参数,且DeepLink配置不完整,修改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" /> <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> </navigation>
删除无参数的deepLink2,避免触发无参数的导航路径。
2. 修复TodoAdapter语法错误
onCreateViewHolder中嵌套return是语法错误,修改为:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val ItemTodoBinding = ItemTodoBinding.inflate(layoutInflater, parent, false) return TodoViewHolder(ItemTodoBinding) }
3. 替换DeepLink为Bundle传参(避免URI编码问题)
在TodoAdapter的标题点击事件中,直接用Bundle传递参数,代替DeepLink:
tvTodoTitle.setOnClickListener { val bundle = Bundle().apply { putString(TASK_BUNDLE_KEY, curTodo.title) putString(TASK_DATE_BUNDLE_KEY, curTodo.date) putString(TASK_DESCR_BUNDLE_KEY, curTodo.description) } findNavController().navigate(R.id.taskDetailFragment, bundle) }
4. 修复TaskDetailFragment初始化时机问题
onCreate方法中binding尚未初始化,将数据赋值逻辑移到onViewCreated,同时处理参数为空的情况:
class TaskDetailFragment : Fragment() { private var _binding: FragmentTaskDetailBinding? = null private val binding: FragmentTaskDetailBinding get() = checkNotNull(_binding) 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) ?: "无描述" binding.taskTitle.text = title binding.selectedDate.text = date binding.selectedDescription.text = descr } }
5. 修复其他潜在问题
- 合并CheckBox的
setOnCheckedChangeListener和setOnClickListener逻辑,避免重复操作数据库,并在主线程更新UI:
cbDone.setOnCheckedChangeListener { _, isChecked -> toggleStrikeThrough(tvTodoTitle, isChecked) curTodo.isChecked = isChecked GlobalScope.launch { todoDao.update(curTodo) if (isChecked) { todoDao.delete(curTodo) todos.remove(curTodo) GlobalScope.launch(Dispatchers.Main) { notifyDataSetChanged() } } } }
- 在TaskListFragment的FragmentResultListener中,确保UI更新在主线程执行:
requireActivity().supportFragmentManager.setFragmentResultListener( "requestKey", this ) { requestKey, bundle -> val newTodo = bundle.getSerializable("bundleKey") as Todo GlobalScope.launch { todoDao.insert(newTodo) GlobalScope.launch(Dispatchers.Main) { todoAdapter.notifyDataSetChanged() } } }
内容的提问来源于stack exchange,提问作者Владислава Черкасова
相关产品推荐
相关产品推荐

