Data transfer between a Fragment and a BottomSheetDialogFragment
can be effectively managed using Dagger
and the Navigation Component
, avoiding the use of data transfer through constructors or interfaces, as well as SharedViewModel
and Hilt
. This approach allows for a focus on dependency injection and state management through standard tools.
In our example, the key component is DaggerBottomSheetDialogFragment
, which provides dependency injection and state management, offering flexibility and control over the process.
open class DaggerBottomSheetDialogFragment : BottomSheetDialogFragment(), HasAndroidInjector { | |
@Inject lateinit var androidInjector: DispatchingAndroidInjector<Any> | |
override fun onAttach(context: Context) { | |
AndroidSupportInjection.inject(this) | |
super.onAttach(context) | |
} | |
override fun androidInjector(): AndroidInjector<Any> { | |
return androidInjector | |
} | |
} |
Using
Dagger
in this context allows for dependency injection, which opens up possibilities for extending functionality. When dependencies need to be injected into BottomSheetDialogFragment
and other components of the application, Dagger
helps maintain architectural cleanliness and flexibility.
Data Transfer
Data transfer between FirstFragment
and SecondBottomSheetFragment
is organized with a focus on bidirectional data flow as follows:
• Sending Data: FirstFragment
sends data to SecondBottomSheetFragment
using the openBottomFragment
method. This method sets the arguments and uses findNavController()
for navigation.
• Receiving Data: Values obtained from SecondBottomSheetFragment
are observed in observeBackStack
, where currentBackStackEntryFlow
is used to track changes in the navigation stack state, followed by updating data in the ViewModel
.
Let’s look at how this is implemented in code:
class FirstFragment : DaggerFragment() { | |
private var _binding: FragmentFirstBinding? = null | |
private val binding get() = _binding!! | |
@Inject lateinit var providerFactory: ViewModelProviderFactory | |
private val viewModel: FirstFragmentViewModel by lazy { | |
ViewModelProvider(this, providerFactory)[FirstFragmentViewModel::class.java] | |
} | |
override fun onCreateView( | |
inflater: LayoutInflater, | |
container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View { | |
_binding = FragmentFirstBinding.inflate(inflater, container, false) | |
return binding.root | |
} | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
setupListeners() | |
observeBackStack() | |
observeViewModel() | |
} | |
private fun setupListeners() { | |
binding.btnOpenBottomFragment.setOnClickListener { openBottomFragment() } | |
} | |
private fun openBottomFragment() { | |
val navController = findNavController() | |
val currentFragmentId = navController.currentDestination?.id | |
if (currentFragmentId == R.id.firstFragment) { | |
val action = FirstFragmentDirections.actionFirstFragmentToSecondBottomSheetFragment( | |
value = binding.tvValue.text.toString() | |
) | |
navController.navigate(action) | |
} | |
} | |
private fun observeBackStack() { | |
lifecycleScope.launch { | |
findNavController().currentBackStackEntryFlow | |
.distinctUntilChanged() | |
.collect { backStackEntry -> | |
backStackEntry.savedStateHandle.getStateFlow(KEY, "") | |
.filter { it.isNotEmpty() } | |
.collect { value -> | |
viewModel.updateValue(value) | |
} | |
} | |
} | |
} | |
private fun observeViewModel() { | |
lifecycleScope.launch { | |
repeatOnLifecycle(Lifecycle.State.STARTED) { | |
viewModel.value.collect { value -> | |
binding.tvValue.text = value | |
} | |
} | |
} | |
} | |
override fun onDestroyView() { | |
super.onDestroyView() | |
_binding = null | |
} | |
} |
Note that in this example, the
openBottomFragment
method uses a check before performing a navigation action. This is necessary because rapid opening or closing of windows may result in an error related to the incorrect state of navigation. This error manifests as java.lang.IllegalArgumentException
if the NavController
attempts to perform navigation when the current fragment location does not support the transition.
Next, let’s consider the example in SecondBottomSheetFragment
, where data from FirstFragment
is received via initArgs
, which initializes the ViewModel
. The processed data is then sent back to FirstFragment
through the sendDataToParentFragment
method. This method allows for updating the user interface in FirstFragment
, ensuring it reflects the changes made in SecondBottomSheetFragment
.
class SecondBottomSheetFragment : DaggerBottomSheetDialogFragment() { | |
private var _binding: FragmentSecondBottomSheetBinding? = null | |
private val binding get() = _binding!! | |
@Inject lateinit var providerFactory: ViewModelProviderFactory | |
private val viewModel: SecondBottomSheetFragmentViewModel by lazy { | |
ViewModelProvider(this, providerFactory)[SecondBottomSheetFragmentViewModel::class.java] | |
} | |
private val args by navArgs<SecondBottomSheetFragmentArgs>() | |
override fun onCreateView( | |
inflater: LayoutInflater, container: ViewGroup?, | |
savedInstanceState: Bundle? | |
): View { | |
_binding = FragmentSecondBottomSheetBinding.inflate(inflater, container, false) | |
return binding.root | |
} | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
checkConfigurationAndDismiss() | |
initArgs() | |
setupListeners() | |
observeViewModel() | |
} | |
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { | |
val dialog = BottomSheetDialog(requireContext(), theme) | |
dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED | |
return dialog | |
} | |
private fun checkConfigurationAndDismiss() { | |
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { | |
findNavController().popBackStack() | |
} | |
} | |
private fun setupListeners() { | |
binding.btnAddValue.setOnClickListener { viewModel.updateValue(Operation.ADD) } | |
binding.btnReduceValue.setOnClickListener { viewModel.updateValue(Operation.REDUCE) } | |
} | |
private fun observeViewModel() { | |
lifecycleScope.launch { | |
repeatOnLifecycle(Lifecycle.State.STARTED) { | |
viewModel.totalValue.collect { value -> | |
binding.tvTotalValue.text = value.toString() | |
sendDataToParentFragment(value.toString()) | |
} | |
} | |
} | |
} | |
private fun initArgs() { | |
args.value?.let { | |
binding.tvTotalValue.text = it | |
viewModel.presetValue(it.toInt()) | |
} | |
} | |
private fun sendDataToParentFragment(result: String) { | |
findNavController().previousBackStackEntry?.savedStateHandle?.set(KEY, result) | |
} | |
override fun onDestroyView() { | |
super.onDestroyView() | |
_binding = null | |
} | |
} |
class SecondBottomSheetFragmentViewModel @Inject constructor( | |
private val application: Application | |
) : ViewModel() { | |
private val TAG = this::class.java.simpleName | |
private val _totalValue = MutableStateFlow(0) | |
val totalValue: StateFlow<Int> get() = _totalValue.asStateFlow() | |
fun presetValue(value: Int) { | |
_totalValue.value = value | |
} | |
fun updateValue(operation: Operation) { | |
when (operation) { | |
Operation.ADD -> _totalValue.value++ | |
Operation.REDUCE -> _totalValue.value-- | |
} | |
} | |
} |
This example demonstrates how to use
Dagger
and the Navigation Component
for managing state and transferring data between fragments and a BottomSheetDialogFragment. The complete code is available at the GitHub link.
Top comments (0)