Hilt注入SharedPreferences时Nullable不匹配错误求助
问题描述
开发跑步追踪App时,通过Hilt在AppModule.kt中实现SharedPreferences的依赖注入,运行项目时触发3个Dagger/Nullable错误,提示android.content.SharedPreferences不可为空,但被标注为@Singleton @Provides @org.jetbrains.annotations.Nullable的方法提供。错误涉及TrackingFragment、SetupFragment的注入逻辑。
错误日志
Task :app:hiltJavaCompileDebug
C:\Users\Yodgorbek\RunTrackerApp\app\build\generated\hilt\component_sources\debug\com\example\runtrackerapp\BaseApplication_HiltComponents.java:136: 错误: [Dagger/Nullable] android.content.SharedPreferences不可为空,但由@Singleton @Provides @org.jetbrains.annotations.Nullable android.content.SharedPreferences com.example.runtrackerapp.di.AppModule.provideSharedPreferences(@dagger.hilt.android.qualifiers.ApplicationContext android.content.Context)提供
public abstract static class SingletonC implements BaseApplication_GeneratedInjector,
^
android.content.SharedPreferences被注入到
com.example.runtrackerapp.di.AppModule.provideWeight(sharedPref)
java.lang.Float被注入到
com.example.runtrackerapp.ui.fragments.TrackingFragment.setWeight(<set-?>)
com.example.runtrackerapp.ui.fragments.TrackingFragment被注入到
com.example.runtrackerapp.ui.fragments.TrackingFragment_GeneratedInjector.injectTrackingFragment(com.example.runtrackerapp.ui.fragments.TrackingFragment) [com.example.runtrackerapp.BaseApplication_HiltComponents.SingletonC → com.example.runtrackerapp.BaseApplication_HiltComponents.ActivityRetainedC → com.example.runtrackerapp.BaseApplication_HiltComponents.ActivityC → com.example.runtrackerapp.BaseApplication_HiltComponents.FragmentC]C:\Users\Yodgorbek\RunTrackerApp\app\build\generated\hilt\component_sources\debug\com\example\runtrackerapp\BaseApplication_HiltComponents.java:136: 错误: [Dagger/Nullable] android.content.SharedPreferences不可为空,但由@Singleton @Provides @org.jetbrains.annotations.Nullable android.content.SharedPreferences com.example.runtrackerapp.di.AppModule.provideSharedPreferences(@dagger.hilt.android.qualifiers.ApplicationContext android.content.Context)提供
public abstract static class SingletonC implements BaseApplication_GeneratedInjector,
^
android.content.SharedPreferences被注入到
com.example.runtrackerapp.ui.fragments.SetupFragment.sharedPref
com.example.runtrackerapp.ui.fragments.SetupFragment被注入到
com.example.runtrackerapp.ui.fragments.SetupFragment_GeneratedInjector.injectSetupFragment(com.example.runtrackerapp.ui.fragments.SetupFragment) [com.example.runtrackerapp.BaseApplication_HiltComponents.SingletonC → com.example.runtrackerapp.BaseApplication_HiltComponents.ActivityRetainedC → com.example.runtrackerapp.BaseApplication_HiltComponents.ActivityC → com.example.runtrackerapp.BaseApplication_HiltComponents.FragmentC]C:\Users\Yodgorbek\RunTrackerApp\app\build\generated\hilt\component_sources\debug\com\example\runtrackerapp.BaseApplication_HiltComponents.java:136: 错误: [Dagger/Nullable] android.content.SharedPreferences不可为空,但由@Singleton @Provides @org.jetbrains.annotations.Nullable android.content.SharedPreferences com.example.runtrackerapp.di.AppModule.provideSharedPreferences(@dagger.hilt.android.qualifiers.ApplicationContext android.content.Context)提供
public abstract static class SingletonC implements BaseApplication_GeneratedInjector,
^
android.content.SharedPreferences被注入到
com.example.runtrackerapp.di.AppModule.provideFirstTimeToggle(sharedPref)
java.lang.Boolean被注入到
com.example.runtrackerapp.ui.fragments.SetupFragment.setFirstAppOpen(<set-?>)
com.example.runtrackerapp.ui.fragments.SetupFragment被注入到
com.example.runtrackerapp.ui.fragments.SetupFragment_GeneratedInjector.injectSetupFragment(com.example.runtrackerapp.ui.fragments.SetupFragment) [com.example.runtrackerapp.BaseApplication_HiltComponents.SingletonC → com.example.runtrackerapp.BaseApplication_HiltComponents.ActivityRetainedC → com.example.runtrackerapp.BaseApplication_HiltComponents.ActivityC → com.example.runtrackerapp.BaseApplication_HiltComponents.FragmentC]
3个错误
相关代码
AppModule.kt
@Module @InstallIn(SingletonComponent::class) object AppModule { @Singleton @Provides fun provideRunningDatabase( @ApplicationContext app: Context ) = Room.databaseBuilder( app, RunningDatabase::class.java, RUNNING_DATABASE_NAME ).build() @Singleton @Provides fun provideRunDao(db: RunningDatabase) = db.getRunDao() // 问题根源:返回Nullable的SharedPreferences,但实际不会为空 @Singleton @Provides fun provideSharedPreferences(@ApplicationContext app: Context): SharedPreferences? { val sharedPreferences = app.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE) return sharedPreferences } @Singleton @Provides fun provideName(sharedPref: SharedPreferences) = sharedPref.getString(KEY_NAME, "") ?: "" @Singleton @Provides fun provideWeight(sharedPref: SharedPreferences) = sharedPref.getFloat(KEY_WEIGHT, 80f) @Singleton @Provides fun provideFirstTimeToggle(sharedPref: SharedPreferences) = sharedPref.getBoolean(KEY_FIRST_TIME_TOGGLE, true) }
SetupFragment.kt
@AndroidEntryPoint class SetupFragment : Fragment() { private var _binding: FragmentSetupBinding? = null private val binding get() = _binding!! @Inject lateinit var sharedPref: SharedPreferences @set:Inject var isFirstAppOpen = true override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { // 加载布局并绑定到_binding _binding = FragmentSetupBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if(!isFirstAppOpen){ val navOptions = NavOptions.Builder() .setPopUpTo(R.id.setupFragment, true) .build() findNavController().navigate( R.id.action_setupFragment_to_runFragment, savedInstanceState, navOptions) } binding.tvContinue.setOnClickListener { val success = writePersonalDataToSharedPref() if (success){ findNavController().navigate(R.id.action_setupFragment_to_runFragment) }else{ Snackbar.make(requireView(), "请填写所有字段", Snackbar.LENGTH_SHORT).show() } } } private fun writePersonalDataToSharedPref(): Boolean { val name = binding.etName.text.toString() val weight = binding.etWeight.text.toString() if(name.isEmpty() || weight.isEmpty()) { return false } sharedPref.edit() .putString(KEY_NAME, name) .putFloat(KEY_WEIGHT, weight.toFloat()) .putBoolean(KEY_FIRST_TIME_TOGGLE, false) .apply() val toolbarText = "出发吧,$name!" (requireActivity() as ActivityMainBinding).tvToolbarTitle.text = toolbarText return true } }
TrackingFragment.kt
@AndroidEntryPoint class TrackingFragment : Fragment(R.layout.fragment_tracking) { private val viewModel: MainViewModel by viewModels() private var isTracking = false private var pathPoints = mutableListOf<Polyline>() private var map: GoogleMap? = null private var curTimeInMillis = 0L private var _binding: FragmentTrackingBinding? = null private val binding get() = _binding!! private var menu: Menu? = null @set:Inject var weight = 80f override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { setHasOptionsMenu(true) _binding = FragmentTrackingBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.mapView.onCreate(savedInstanceState) binding.btnToggleRun.setOnClickListener { toggleRun() } binding.btnFinishRun.setOnClickListener { zoomTo查看完整轨迹() endRunAndSaveToDb() } binding.mapView.getMapAsync { map = it addAllPolylines() } subscribeToObservers() } private fun subscribeToObservers() { TrackingService.isTracking.observe(viewLifecycleOwner, Observer { updateTracking(it) }) TrackingService.pathPoints.observe(viewLifecycleOwner, Observer { pathPoints = it addLatestPolyline() moveCameraToUser() }) TrackingService.timeRunInMillis.observe(viewLifecycleOwner, Observer { curTimeInMillis = it val formattedTime = TrackingUtility.getFormattedStopWatchTime(curTimeInMillis, true) binding.tvTimer.text = formattedTime }) } private fun toggleRun() { if(isTracking) { menu?.getItem(0)?.isVisible = true sendCommandToService(ACTION_PAUSE_SERVICE) } else { sendCommandToService(ACTION_START_OR_RESUME_SERVICE) } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { super.onCreateOptionsMenu(menu, inflater) inflater.inflate(R.menu.toolbar_tracking_menu, menu) this.menu = menu } override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) if(curTimeInMillis > 0L) { this.menu?.getItem(0)?.isVisible = true } } override fun onOptionsItemSelected(item: MenuItem): Boolean { when(item.itemId) { R.id.miCancelTracking -> { showCancelTrackingDialog() } } return super.onOptionsItemSelected(item) } private fun showCancelTrackingDialog() { val dialog = MaterialAlertDialogBuilder(requireContext(), R.style.AlertDialogTheme) .setTitle("取消跑步?") .setMessage("确定要取消当前跑步并删除所有数据吗?") .setIcon(R.drawable.ic_delete) .setPositiveButton("是") { _, _ -> stopRun() } .setNegativeButton("否") { dialogInterface, _ -> dialogInterface.cancel() } .create() dialog.show() } private fun stopRun() { sendCommandToService(ACTION_STOP_SERVICE) findNavController().navigate(R.id.action_trackingFragment_to_runFragment) } private fun updateTracking(isTracking: Boolean) { this.isTracking = isTracking if(!isTracking) { binding.btnToggleRun.text = "开始" binding.btnFinishRun.visibility = View.VISIBLE } else { binding.btnToggleRun.text = "停止" menu?.getItem(0)?.isVisible = true binding.btnFinishRun.visibility = View.GONE } } private fun moveCameraToUser() { if(pathPoints.isNotEmpty() && pathPoints.last().isNotEmpty()) { map?.animateCamera( CameraUpdateFactory.newLatLngZoom( pathPoints.last().last(), MAP_ZOOM ) ) } } private fun zoomTo查看完整轨迹(){ val bounds = LatLngBounds.builder() for(polyline in pathPoints){ for(pos in polyline){ bounds.include(pos) } } map?.moveCamera( CameraUpdateFactory.newLatLngBounds( bounds.build(), binding.mapView.width, binding.mapView.height, (binding.mapView.height * 0.05f).toInt() ) ) } private fun endRunAndSaveToDb(){ map?.snapshot { bmp -> var distanceInMeters = 0 for (polyline in pathPoints){ distanceInMeters+= TrackingUtility.calculatePolylineLength(polyline).toInt() } val avgSpeed = round((distanceInMeters / 1000f) / (curTimeInMillis / 1000f / 60 / 60) * 10) / 10f val dataTimestamp = Calendar.getInstance().timeInMillis val caloriesBurned = ((distanceInMeters / 1000f) * weight).toInt() val run = Run(bmp,dataTimestamp, avgSpeed,distanceInMeters, curTimeInMillis, caloriesBurned) viewModel.insertRun(run) Snackbar.make( requireActivity().findViewById(R.id.rootView), "跑步已成功保存", Snackbar.LENGTH_LONG ).show() stopRun() } } private fun addAllPolylines() { for(polyline in pathPoints) { val polylineOptions = PolylineOptions() .color(POLYLINE_COLOR) .width(POLYLINE_WIDTH) .addAll(polyline) map?.addPolyline(polylineOptions) } } private fun addLatestPolyline() { if(pathPoints.isNotEmpty() && pathPoints.last().size > 1) { val preLastLatLng = pathPoints.last()[pathPoints.last().size - 2] val lastLatLng = pathPoints.last().last() val polylineOptions = PolylineOptions() .color(POLYLINE_COLOR) .width(POLYLINE_WIDTH) .add(preLastLatLng) .add(lastLatLng) map?.addPolyline(polylineOptions) } } private fun sendCommandToService(action: String) = Intent(requireContext(), TrackingService::class.java).also { it.action = action requireContext().startService(it) } override fun onResume() { super.onResume() binding.mapView?.onResume() } override fun onStart() { super.onStart() binding.mapView?.onStart() } override fun onStop() { super.onStop() binding.mapView?.onStop() } override fun onPause() { super.onPause() binding.mapView?.onPause() } override fun onLowMemory() { super.onLowMemory() binding.mapView?.onLowMemory() } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) binding.mapView?.onSaveInstanceState(outState) } }
问题原因与解决方案
问题根源
provideSharedPreferences方法返回了可空类型SharedPreferences?,但Android系统的Context.getSharedPreferences()方法实际不会返回null。同时,所有依赖这个实例的注入点(比如SetupFragment的lateinit var sharedPref、provideName等方法的参数)都是非可空类型,Dagger/Hilt会严格检查可空性匹配,因此抛出错误。
修复步骤
修改AppModule.kt中的provideSharedPreferences方法,将返回类型改为非可空的SharedPreferences:
@Singleton @Provides fun provideSharedPreferences(@ApplicationContext app: Context): SharedPreferences { return app.getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE) }
这样修改后,Dagger/Hilt会确认提供的SharedPreferences实例非空,与所有依赖它的注入点类型匹配,错误即可解决。
内容的提问来源于stack exchange,提问作者Edgar

