Android jetpack navigation component result from dialog - android

So far I'm successfully able to navigate to dialogs and back using only navigation component. The problem is that, I have to do some stuff in dialog and return result to the fragment where dialog was called from.
One way is to use shared viewmodel. But for that I have to use .of(activity) which leaves my app with a singleton taking up memory, even when I no longer need it.
Another way is to override show(fragmentManager, id) method, get access to fragment manager and from it, access to previous fragment, which could then be set as targetfragment. I've used targetFragment approach before where I would implement a callback interface, so my dialog could notify targetFragment about result. But in navigation component approach it feels hacky and might stop working at one point or another.
Any other ways to do what I want? Maybe there's a way to fix issue on first approach?

Thanks to #NataTse and also the official docs, i came up with the extensions so that hopefully less boilerplate code to write:
fun <T>Fragment.setNavigationResult(key: String, value: T) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(
key,
value
)
}
fun <T>Fragment.getNavigationResult(#IdRes id: Int, key: String, onResult: (result: T) -> Unit) {
val navBackStackEntry = findNavController().getBackStackEntry(id)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains(key)
) {
val result = navBackStackEntry.savedStateHandle.get<T>(key)
result?.let(onResult)
navBackStackEntry.savedStateHandle.remove<T>(key)
}
}
navBackStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}

In Navigation 2.3.0-alpha02 and higher, NavBackStackEntry gives access to a SavedStateHandle. A SavedStateHandle is a key-value map that can be used to store and retrieve data. These values persist through process death, including configuration changes, and remain available through the same object. By using the given SavedStateHandle, you can access and pass data between destinations. This is especially useful as a mechanism to get data back from a destination after it is popped off the stack.
To pass data back to Destination A from Destination B, first set up Destination A to listen for a result on its SavedStateHandle. To do so, retrieve the NavBackStackEntry by using the getCurrentBackStackEntry() API and then observe the LiveData provided by SavedStateHandle.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController();
// We use a String here, but any type that can be put in a Bundle is supported
navController.currentBackStackEntry?.savedStateHandle?.getLiveData("key")?.observe(
viewLifecycleOwner) { result ->
// Do something with the result.
}
}
In Destination B, you must set the result on the SavedStateHandle of Destination A by using the getPreviousBackStackEntry() API.
navController.previousBackStackEntry?.savedStateHandle?.set("key", result)

When you use Navigation Component with dialogs, this part of code looks not so good (for me it returned nothing)
navController.currentBackStackEntry?.savedStateHandle?.getLiveData("key")?.observe(
viewLifecycleOwner) { result ->
// Do something with the result.}
You need to try way from official docs and it help me a lot
This part is working for me:
val navBackStackEntry = navController.getBackStackEntry(R.id.target_fragment_id)
// Create observer and add it to the NavBackStackEntry's lifecycle
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains("key")
) {
val result =
navBackStackEntry.savedStateHandle.get<Boolean>("key")
// Do something with the result
}
}
navBackStackEntry.lifecycle.addObserver(observer)
// As addObserver() does not automatically remove the observer, we
// call removeObserver() manually when the view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
And in your dialog:
navController.previousBackStackEntry?.savedStateHandle?.set(
"key",
true
)

Related

changing value of a LiveData on click of button only. But the observer's onChange is getting called even on onCreateView of fragment in Kotlin Android

I have a LiveData in my ViewModel:-
private val _toastMessage = MutableLiveData<Long>()
val toastMessage
get() = _toastMessage
And this is the only way I am changing it's value(on click of a submit button in the fragment):-
fun onSubmitClicked(<params>){
Log.i(LOG_TAG, "submit button clicked")
uiScope.launch {
if(!myChecksForEditTextValuesSucceeded())
{
_toastMessage.value = 0
}else{
_toastMessage.value = 1
}
}
}
And in the fragment, I have an observer for this LiveData:-
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { it->
when{
(it.compareTo(0) == 0) -> Toast.makeText(context, resources.getString(R.string.toast_msg_transaction_not_inserted), Toast.LENGTH_SHORT).show()
else -> Toast.makeText(context, resources.getString(R.string.toast_msg_transaction_inserted), Toast.LENGTH_SHORT).show()
}
})
Ideally, I am expecting the onChange of this Observer to be called only on clicking the submit button on my fragment. But, as I can see, it is also getting called even on onCreateView of my fragment.
What could be the possible reasons for this?
The issue is that LiveData pushes new values while you're observing it, but it also pushes the most recent value when you first observe it, or if the observer's Lifecycle resumes and the data has changed since it was paused.
So when you set toastMessage's value to 1, it stays that way - and ViewModels have a longer lifetime than Fragments (that's the whole point!) so when your Fragment gets recreated, it observes the current value of toastMessage, sees that it's currently 1, and shows a Toast.
The problem is you don't want to use it as a persistent data state - you want it to be a one-shot event that you consume when you observe it, so the Toast is only shown once in response to a button press. This is one of the tricky things about LiveData and there have been a bunch of workarounds, classes, libraries etc built around making it work
There's an old post here from one of the Android developers discussing the problem with this use case, and the workarounds available and where they fall short - in case anyone is interested! But like it says at the top, that's all outdated, and they recommend following the official guidelines.
The official way basically goes:
something triggers an event on the ViewModel
the VM updates the UI state including a message to be displayed
the UI observes this update, displays the message, and informs the VM it's been displayed
the VM updates the UI state with the message removed
That's not the only way to handle consumable events, but it's what they're recommending, and it's fairly simple. So you'd want to do something like this:
// making this nullable so we can have a "no message" state
private val _toastMessage = MutableLiveData<Long?>(null)
// you should specify the type here btw, as LiveData instead of MutableLiveData -
// that's the reason for making the Mutable reference private and having a public version
val toastMessage: LiveData<Long?>
get() = _toastMessage
// call this when the current message has been shown
fun messageDisplayed() {
_toastMessage.value = null
}
// make a nice display function to avoid repetition
fun displayToast(#StringRes resId: Int) {
Toast.makeText(context, resources.getString(resId), Toast.LENGTH_SHORT).show()
// remember to tell the VM it's been displayed
transactionViewModel.messageDisplayed()
}
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { it->
// if the message code is null we just don't do anything
when(it) {
0 -> displayToast(R.string.toast_msg_transaction_not_inserted)
1 -> displayToast(R.string.toast_msg_transaction_inserted)
}
})
You also might want to create an enum of Toast states instead of just using numbers, way more readable - you can even put their string IDs in the enum:
enum class TransactionMessage(#StringRes val stringId: Int) {
INSERTED(R.string.toast_msg_transaction_inserted),
NOT_INSERTED(R.string.toast_msg_transaction_not_inserted)
}
private val _toastMessage = MutableLiveData<TransactionMessage?>(null)
val toastMessage: LiveData<TransactionMessage?>
get() = _toastMessage
uiScope.launch {
if(!myChecksForEditTextValuesSucceeded()) toastMessage.value = NOT_INSERTED
else _toastMessage.value = INSERTED
}
transactionViewModel.toastMessage.observe(viewLifecycleOwner, Observer { message ->
message?.let { displayToast(it.stringId) }
// or if you're not putting the string resource IDs in the enum:
when(message) {
NOT_INSERTED -> displayToast(R.string.toast_msg_transaction_not_inserted)
INSERTED -> displayToast(R.string.toast_msg_transaction_inserted)
}
})
It can be a bit clearer and self-documenting compared to just using numbers, y'know?

Pass data to previous composable in Android Compose

I will take a simple sample.
I have 2 Screens: Screen A and Screen B. From Screen A, I open Screen B. And when I return Screen B to Screen A, I want to transfer data back to Screen A.
With Android Fragment, I can use Shared ViewModel or Fragment Result API to do this.
But with Android Compose, the Fragment Result Api is not in Compose. With using Shard ViewModel, what lifecycle do I have to attach Shared ViewModel so it can keep alive? Activity, ... or something else.
Or is there another way to do this?
If you use jetpack navigation, you can pass back data by adding it to the previous back stack entry's savedStateHandle. (Documentation)
Screen B passes data back:
composable("B") {
ComposableB(
popBackStack = { data ->
// Pass data back to A
navController.previousBackStackEntry
?.savedStateHandle
?.set("key", data)
navController.popBackStack()
}
)
}
Screen A Receives data:
composable("A") { backStackEntry ->
// get data passed back from B
val data: T by backStackEntry
.savedStateHandle
.getLiveData<T>("key")
.observeAsState()
ComposableA(
data = data,
navToB = {
// optional: clear data so LiveData emits
// even if same value is passed again
backStackEntry.savedStateHandle.remove("key")
// navigate ...
}
)
}
Replace "key" with a unique string, T with the type of your data and data with your data.
All of your compose composition operations happens within a single activity view hierarchy thus your ViewModel lifecycle will inevitably be bound to that root activity. It can actually be accessed from your composition through LocalLifecycleOwner.current.
Keep in mind that Compose is a totally different paradigm than activity/fragment, you can indeed share ViewModel across composables but for the sake of keeping those simple you can also just "share" data simply by passing states using mutable values and triggering recomposition.
class MySharedViewModel(...) : ViewModel() {
var sharedState by mutableStateOf<Boolean>(...)
}
#Composable
fun MySharedViewModel(viewModel: MySharedViewModel = viewModel()) {
// guessing you already have your own screen display logic
// This also works with compose-navigator
ComposableA(stateResult = viewModel.sharedState)
ComposableB(onUpdate = { viewModel.sharedState = false })
}
fun ComposableA(stateResult: Boolean) {
....
}
fun ComposableB(onUpdate: () -> Unit) {
Button(onClick = { onUpdate() }) {
Text("Update ComposableA result")
}
}
Here you'll find further documentation on managing states with compose
Let's say there are two screens.
1 - FirstScreen it will receive some data and residing on bottom in back stack user will land here from Second screen by press back button.
2 - SecondScreen it will send/attach some data to be received on previous first screen.
Lets start from second screen sending data, for that you can do something like this:
navController.previousBackStackEntry
?.savedStateHandle
?.set("key", viewModel.getFilterSelection().toString())
navController.popBackStack()
Now lets catch that data on first screen for that you can do some thing like this:
if (navController.currentBackStackEntry!!.savedStateHandle.contains("key")) {
val keyData =
navController.currentBackStackEntry!!.savedStateHandle.get<String>(
"key"
) ?: ""
}
Worked perfectly for me.

Collect transformed StateFlow in Composable

There is function collectAsState() applicable to a StateFlow property in order to observe it in a Composable.
A composable requires a StateFlow because StateFlow guarantees an initial value. A Flow doesn't come with that guarantee.
Now, what is the way to go if I have a StateFlow property but I want to apply an operator (like map) before collecting the Flow in the Composable?
Here an example:
Let's say a repository exposes a StateFlow<MyClass>
val myClassStateFlow: StateFlow<MyClass>
data class MyClass(val a: String)
... and a view model has a dependency on the repository and wants to expose only the property a to its Composable...
val aFlow = myClassState.Flow.map { it.a } // <- this is of type Flow<String>
The map operator changes the type from StateFlow<MyClass> to Flow<String>.
Is it semantically justified that aFlow has no initial value anymore? After all its first emission is derived from the initial value of myClassStateFlow.
It's required to convert Flow back into StateFlow at some point. Which is the more idiomatic place for this?
In the view model using stateIn()? How would the code look like?
In the composable using collectAsState(initial: MyClass) and come up with an initial value (although myClassStateFlow had an initial value)?
See this issue on GitHub
Currently there is no built-in way to transform StateFlows, only Flows. But you can write your own.
Way I ended up solving was to use the example in that post.
First create a notion of a DerivedStateFlow.
class DerivedStateFlow<T>(
private val getValue: () -> T,
private val flow: Flow<T>
) : StateFlow<T> {
override val replayCache: List<T>
get () = listOf(value)
override val value: T
get () = getValue()
#InternalCoroutinesApi
override suspend fun collect(collector: FlowCollector<T>) {
flow.collect(collector)
}
}
Then have an extension on StateFlow like the current map extension on Flow
fun <T1, R> StateFlow<T1>.mapState(transform: (a: T1) -> R): StateFlow<R> {
return DerivedStateFlow(
getValue = { transform(this.value) },
flow = this.map { a -> transform(a) }
)
}
Now in your Repository or ViewModel, you can use it as below.
class MyViewModel( ... ) {
private val originalStateFlow:StateFlow<SomeT> = ...
val someStateFlowtoExposeToCompose =
originalStateFlow
.mapState { item ->
yourTransform(item)
}
}
Now you can consume it as you expect in Compose without any special work, since it returns a StateFlow.

Android navigation component: How to get the current navigation graph's id?

I am using nested navigation graphs in order to scope and share my viewmodels across a set of fragments.
I also have a BaseFragment class which obtains the reference to the required viewmodel:
fun provideViewModel() : VM {
return if(viewModelScopeGraphId != null) {
ViewModelProvider(findNavController().getViewModelStoreOwner(viewModelScopeGraphId!!)).get(viewModelClass)
} else {
ViewModelProvider(this).get(viewModelClass)
}
}
I can override a property, viewModelScopeGraphId, if I need the viewmodel to be scoped to the navigation graph (or nested navigation graph) with that specific id.
Ideally I would just want to set a boolean flag like useScopedViewModel and obtain the id of the current navigation graph, for example:
fun provideViewModel() : VM {
return if(useScopedViewModel) {
ViewModelProvider(findNavController().getViewModelStoreOwner(getCurrentNavGraphId())).get(viewModelClass)
} else {
ViewModelProvider(this).get(viewModelClass)
}
}
I have tried using navController.graph.id to get the current graph id, but it seems the id I get from there does not match up with my resource id's (eg. R.id.nav_graph). Is there something I am missing?
I have a similar issue (more context of my case at the end), tried a few solutions, none successful.
In the end, I give up and just give the graph id as a parameter for the Fragment.
As it could help you to achieve your own solution, so my solution looks like the following:
Have to create this extension, as the original navGraphViewModels only accepts a #IdRes navGraphId: Int and I want to lazy load the arguments, etc. (Also I've simplified removing the factoryProducer from the arguments, as so far I will not use it for our solution.
inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
noinline graphIdProducer: () -> Int
): Lazy<VM> {
val backStackEntry by lazy {
findNavController().getBackStackEntry(graphIdProducer())
}
val storeProducer: () -> ViewModelStore = {
backStackEntry.viewModelStore
}
return createViewModelLazy(VM::class, storeProducer, {
backStackEntry.defaultViewModelProviderFactory
})
}
In my fragment where I want to use it, I recover the Arguments using the navArgs, and recover the ViewModel using the extension above:
private val listenerViewModel: ListenerViewModel by navGraphViewModels {
navArgs.graphId
}
private val navArgs: MyFragmentArgs by navArgs()
And to whoever needs to "listen" for that ViewModel, can simply load using the Navigation navGraphViewModels:
private val listenerViewModel: ListenerViewModel by navGraphViewModels(
R.id.my_graph_a
)
And from another graph/fragment I simple do:
private val listenerViewModel: ListenerViewModel by navGraphViewModels(
R.id.my_graph_b
)
To explain a little the context of my case:
I have two distinct Fragments, where the user can click in a Country selection.
Each of these Fragments is inside a different Graph, as they are different flows.
The Country selection is a Fragment where load the supported countries list from an API, displays it, pre-select any previous user selection (also given as a FragmentArgs), and the user can change the selection, which implies coming back to the previous screen, with the newly selected value or just come back not triggering anything here.
I'm aware and expecting the release of https://issuetracker.google.com/issues/79672220, but as it is today (March-2020) it is only available in alpha.
You may try to check it with graph's start destination.
when (navController.graph.startDestinationId) {
R.id.firstFragmentOfFirstGraph -> { /* First graph */ }
R.id.firstFragmentOfSecondGraph -> { /* Second graph */}
}

How to create LiveData which emits a single event and notifies only last subscribed observer?

I created live data which emits a single event as in this example.
My question is next:
How to notify only last subscribed observer when the value in the LiveData changes?
What comes to my mind is to store observers in the linked list in SingleLiveData class and then to call super.observe only if a passed observer is the same as the last element of the list.
I'm not sure if this is the best approach.
I want to use this mechanism to propagate FAB click events from activity to the fragments which are shown inside of the ViewPager. Fragments are dynamically added to view pager adapter, so let's say that we know the order of the fragments.
In the end, I found a workaround for this problem. I had to move away from the live data that emits a single event since it couldn't behave the way I needed it to behave.
Instead of this, I used simple mutable live data which emits an event object which wraps a data as in the last paragraph of this article by Jose Alcérreca.
I'm showing fragments in a view pager so I have only one visible fragment at the time.
So my view model looks like this:
class ActionViewModel : ViewModel() {
private val onCreateLiveData: MutableLiveData<Event<String>> = MutableLiveData()
fun observeOnCreateEvent(): LiveData<Event<String>> = onCreateLiveData
fun onCreateCollectionClick(message: String) {
this.onCreateLiveData.value = Event(message)
}
}
Event wrapper class implementation looks like this:
/*Used as a wrapper for data that is exposed via a LiveData that represents an
event.*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
In fragments now we can observe events like this:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionViewModel::class.java)
actionViewModel.observeOnCreateEvent()
.observe(this, Observer {
it?.takeIf { userVisibleHint }?.getContentIfNotHandled()?.let {
//DO what ever is needed
}
})
}
Fragment userVisibleHint property will return true if the fragment is currently visible to the user. Since we are only showing one fragment at the time this works for us. This means that the fragment will only access the event data if it is visible.
Also, implementation of the Event wrapper allows only one read of the value, so that every next time Observer gets this event, its value will be null and we'll ignore it.
Conclusion: This way we are simulating a single event live data which notifies only last subscribed observer.
If you're using Kotlin, you can replace LiveData with Flow. StateFlow can be used to replace regular LiveData, while SharedFlow can be used for stateless events. It will also provide you null safety and all the operators and configurations that come with Flow.
The migration is described here among other places. Here's a basic example:
ViewModel:
interface MyViewModel {
val myData: StateFlow<MyData>
val myEvents: SharedFlow<MyEvent>
}
class MyViewModelImpl: MyViewModel {
override val myData = MutableStateFlow(MyData())
override val myEvents = MutableSharedFlow<MyEvent>(replay = 0, extraBufferCapacity = 1, BufferOverflow.DROP_OLDEST)
/*
* Do stuff
*/
}
Activity:
lifecycleScope.launch {
myData.collect {
// handle stateful data
}
}
lifecycleScope.launch {
myEvents.collect {
// handle stateless events
}
}
Note that lifecycleScope requires the appropriate ktx dependency.
Herer's some more reading about Flow in Android.
I found solution for me in LD extension:
fun <T> LiveData<T>.observeAsEvent(owner: LifecycleOwner, observer: Observer<in T>) {
var previousKey: Any? = value?: NULL
observe(owner) { value ->
if (previousKey == NULL || previousKey != value) {
previousKey = value
observer.onChanged(value)
}
}
}
private const val NULL = "NULL"
Usage for this:
viewModel.resultLiveData.observeAsEvent(viewLifecycleOwner) {
...
}
I crafted a solution, feel free to take a look
https://github.com/ueen/LiveEvent
I've created a library to handle the most common cases that we might encounter while working with event-driven data scenarios
https://github.com/javaherisaber/LiveX
It contains the following types of classes:
LiveData
LiveEvent
OneShotLiveEvent
SingleLiveEvent
Multiple observers can register, all of them receive the event based on lifecycle
Multiple observers can register, each one receive the event only once
Only one observer can register and receive the event only once
Multiple observers can register, only the first one receive the event

Categories

Resources