You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Hilt注入SharedPreferences时Nullable不匹配错误求助

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 20:24:31