Search code examples
androidkotlindagger-2

Error [Dagger/DuplicateBindings] when working with enum


My android app has enum class Specialization:

enum class Specialization {
    DEVELOPER, MANAGER
}

The values of this class are passed to the viewmodel, which makes a request to the repository based on this:

class EmployeesViewModel @Inject constructor(
    private val typeSpecialization: Specialization,
    private val getEmployeesListUseCase: GetEmployeesListUseCase
): ViewModel() {

    val screenState: Flow<EmployeesFragmentScreenState> = getEmployeesListUseCase(typeSpecialization)
        .filter { it.isNotEmpty() }
        .map { EmployeesFragmentScreenState.Content(employees = it) as EmployeesFragmentScreenState }
        .onStart { emit(EmployeesFragmentScreenState.Loading) }

}

RecycleView:

override fun onBindViewHolder(
        holder: SpecializationItemViewHolder,
        position: Int
    ) {
        val specItem = getItem(position)
        val binding = holder.binding

        binding.specializationName.text = specItem.specializationName
        binding.idspec.text = specItem.specialty_id.toString()

        binding.root.setOnClickListener {
            onSpecializationClickListener?.invoke(specItem.specialty_id)
        }

    }

SpecializationListFragment:

class SpecializationListFragment : Fragment() {

    private var _binding: FragmentSpecializationListBinding? = null
    private val binding: FragmentSpecializationListBinding
        get() = _binding ?: throw RuntimeException("FragmentSpecializationListBinding is null")

    private lateinit var specializationListAdapter: SpecializationListAdapter

    @Inject
    lateinit var viewModelFactory: ViewModelFactory

    private val component by lazy{
        (requireActivity().application as CoreApplication).component
    }

    lateinit var viewModel: SpecializationViewModel

    override fun onAttach(context: Context) {
        component.inject(this)
        super.onAttach(context)
    }
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentSpecializationListBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel = ViewModelProvider(this, viewModelFactory)[SpecializationViewModel::class.java]
        setupRecyclerView()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.RESUMED){
                viewModel.screenState.collect{
                    when(it){
                        is SpecializationsFragmentScreenState.Content -> {
                            //Toast.makeText(requireContext(), "Spec Content", Toast.LENGTH_SHORT).show()
                            binding.progressSpecLayout.visibility = View.GONE
                            specializationListAdapter.submitList(it.specializations)
                        }
                        is SpecializationsFragmentScreenState.Loading -> {
                            binding.progressSpecLayout.visibility = View.VISIBLE
                        }
                    }
                }
            }
        }
    }

    private fun setupRecyclerView(){
        with(binding.rvSpList){
            specializationListAdapter = SpecializationListAdapter()
            adapter = specializationListAdapter
        }
        setupClickListener()
    }

    private fun setupClickListener(){
        specializationListAdapter.onSpecializationClickListener = {
            launchEmployeesListFragment(it)
        }
    }

    private fun launchEmployeesListFragment(typeSpecialization: Int){
        val type = if(typeSpecialization == 101){
            Specialization.MANAGER
        }else{
            Specialization.DEVELOPER
        }
        findNavController().navigate(SpecializationListFragmentDirections.actionSpecializationListFragmentToEmployeesListFragment(type))
    }

}

EmployeesListFragment:

class EmployeesListFragment : Fragment() {

    private var _binding: FragmentEmployeesListBinding? = null
    private val binding: FragmentEmployeesListBinding
        get() = _binding ?: throw RuntimeException("FragmentEmployeesListBinding is null")

    private val args by navArgs<EmployeesListFragmentArgs>()


    @Inject
    lateinit var viewModelFactoryD: ViewModelFactory

    private val component by lazy{
        (requireActivity().application as CoreApplication).component
    }

    lateinit var viewModel: EmployeesViewModel

    private lateinit var employeeListAdapter: EmployeesListAdapter

    override fun onAttach(context: Context) {
        component.inject(this)
        super.onAttach(context)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentEmployeesListBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel = ViewModelProvider(this, viewModelFactoryD)[EmployeesViewModel::class.java]

        setupRecyclerView()

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.RESUMED){
                viewModel.screenState.collect{
                    when(it){
                        is EmployeesFragmentScreenState.Content ->{
                            binding.progressLayout.visibility = View.GONE
                            employeeListAdapter.submitList(it.employees)
                        }
                        is EmployeesFragmentScreenState.Loading ->{
                            binding.progressLayout.visibility = View.VISIBLE
                        }
                    }
                }
            }
        }

    }


    private fun setupRecyclerView(){
        with(binding.rvEmpList){
            employeeListAdapter = EmployeesListAdapter()
            adapter = employeeListAdapter
        }
        setupClickListener()
    }

    private fun setupClickListener(){
        employeeListAdapter.onEmployeeClickListener = {
            launchEmployeeFragment(it)
        }
    }

    private fun launchEmployeeFragment(employee: Employee){
        findNavController().navigate(EmployeesListFragmentDirections.actionEmployeesListFragmentToEmployeeFragment(employee))
    }

}

Next, I just get the specialty type on the next screen and pass it to the ViewModel, which makes a request to the repository via UseCase. Depending on the type, employees of a certain specialty will be received.

How to handle this situation with Dagger 2? When trying to write a method for provides, the error [Dagger/DuplicateBindings] naturally occurs:

@Module
class DomainModule {

    @Provides
    fun provideSpecializationDeveloper(): Specialization {
        return Specialization.DEVELOPER
    }

    @Provides
    fun provideSpecializationManager(): Specialization {
        return Specialization.MANAGER
    }
}

Solution

  • What you were actually asking is how to pass the navigation argument to the view model of another fragment.

    What you want is SavedStateHandle API + Navigation Safe Args.

    class EmployeesViewModel @Inject constructor(
        private val savedStateHandle: SavedStateHandle,
        private val getEmployeesListUseCase: GetEmployeesListUseCase
    ): ViewModel() {
    
        val typeSpecialization: Specialization = EmployeesListFragmentArgs.fromSavedStateHandle(savedStateHandle)).type
    
        val screenState: Flow<EmployeesFragmentScreenState> = getEmployeesListUseCase(typeSpecialization)
            .filter { it.isNotEmpty() }
            .map { EmployeesFragmentScreenState.Content(employees = it) as EmployeesFragmentScreenState }
            .onStart { emit(EmployeesFragmentScreenState.Loading) }
    }
    

    You might have some issues passing the enum direcly - I would just pass the Int and map it in view model to safe the hassle - if you want to go that route - here is how.

    val type = if (typeSpecialization == 101) {
        Specialization.MANAGER
    } else {
        Specialization.DEVELOPER
    }
    

    But in order to do this you will have to update your ViewModelFactory to SavedStateViewModelFactory

    I know it is a lot of changes to here is something that will work with minimal changes:

    class EmployeesViewModel @Inject constructor(
        private val getEmployeesListUseCase: GetEmployeesListUseCase
    ): ViewModel() {
    
        var isInitalized: Boolean = false
        
        private _screenState: MutableStateFlow<EmployeesFragmentScreenState> = MutableStateFlow(EmployeesFragmentScreenState.Loading)
        val screenState: StateFlow<EmployeesFragmentScreenState> = _screenState.asStateFlow()
    
        fun initialize(typeSpecialization: Specialization) {
            if (!isInitialized) {
                isInitialized = true
                viewModelScope.launch {
                    getEmployeesListUseCase(typeSpecialization)
                        .filter { it.isNotEmpty() }
                        .map { EmployeesFragmentScreenState.Content(employees = it) as EmployeesFragmentScreenState }
                        .collect { newState ->
                            _screenState.update { newState }
                        }
                }
            }
        }
    }
    

    Fragment:

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel = ViewModelProvider(this, viewModelFactoryD)[EmployeesViewModel::class.java]
        viewModel.initialize(arguments?.getInt("type")) //where "type" is the name of the argument in navGraph
        setupRecyclerView()
    
        viewModel.screenState.collectAsStateWithLifecycle {
            when(it) {
                is EmployeesFragmentScreenState.Content ->{
                    binding.progressLayout.visibility = View.GONE
                    employeeListAdapter.submitList(it.employees)
                }
                is EmployeesFragmentScreenState.Loading ->{
                    binding.progressLayout.visibility = View.VISIBLE
                }
            }
        }
    
    }