1

The question about UI layer and unidirectional data flow (UDF). Let's we have DetailFragment with EditText:

  • When user open existing detail, text filled from StateFlow<DetailUiState> by ViewModel.

  • Now user changing text, DetailFragment send new text to ViewModel that update UI state and due to collect fragment receive updated state. So fragment call setText that triggers to send same text to ViewModel again.

  • If we haven't update UI State when receive new text, for any configuration changes (screen rotation) fragment receive old text value when re-create.

How to exit from such loop?

data class DetailUiState(
    val text: String
)

class DetailViewModel {

   private val _uiState = MutableStateFlow(DetailUiState("first text"))
   val uiState = _uiState.asStateFlow()

   fun onTextChanged(newValue: String) {
       _uiState.value = DetailUiState(newValue)
   }
}

class DetailFragment {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        repeatOnStartedLatest(viewModel.uiState) { // flow.collect shortcut
            binding.editText.setText(it.text)
        }
       
        binding.editText.doAfterTextChanged {
            viewModel.onTextChanged(binding.editText.getTextAsString())
        }
    }

}
Viewed
  • 1,159
  • 3
  • 16
  • 43
  • Would `kotlinx.coroutines.channels.Channel` be a better fit to your problem instead of `Flow`? – Onik Jun 02 '23 at 20:08
  • You should probably have some kind of `updating` flag that you can set before updating the `EditText`, then unset after. Then have your `doAfterTextChanged` function check that flag before doing anything - if an UI state update is happening, don't push it back to the VM as a new user input event (because it's not!) – cactustictacs Jun 03 '23 at 00:00

1 Answers1

1

A better way to deal with this problem is to use the two-way databinding feature which saves the text your typed and avoid feedback loops.

In your ViewModel class have a field such as:

val text = MutableStateFlow("First value")

In the layout file of the fragment:


    <data class=".FragmentBinding">
        <variable
            name="vm"
            type="com.sample.YourViewModel" />
    </data>

    <EditText
        android:text="@={vm.text}" <!-- 2-way text binding -->
        />

Now, from the Fragment class pass in the ViewModel reference and lifecycle owner:

private val vm: YourViewModel by viewModels()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
    binding = FragmentBinding.inflate(inflater, container, false)
    binding.vm = vm
    binding.lifecycleOwner = viewLifecycleOwner
    return binding.root
}
Samuel Robert
  • 10,106
  • 7
  • 39
  • 60