savedStateHandle only have args which is defined in navigation graph - android

Dependency
androidx.navigation:navigation-fragment-ktx:1.5.5
Fragment
override val viewModel by viewModels<CreateRequestViewModel> {
SavedStateViewModelFactory(
requireActivity().application, this,
bundleOf("groupId" to 1, "type" to "Deposit")
)
}
Navigation Graph
<dialog
android:id="#+id/createRequestSheet"
android:name="com.xxx.create.CreateRequestSheet"
tools:layout="#layout/bottom_sheet_create">
<argument
android:name="group_id"
app:argType="string"
app:nullable="false" />
<deepLink
android:id="#+id/deepLink"
app:uri="app://request/create/args?group_id={group_id}" />
</dialog>
ViewModel
#HiltViewModel
class CreateRequestViewModel #Inject constructor(application: Application, private val savedStateHandle: SavedStateHandle) :
AndroidViewModel(application) {
fun getArgs(): String {
val type = savedStateHandle.get<String>("type")
Log.v("request Type::", type?:"")
return type
}}
From the above codes, I defined groupId argument in nav graph and when viewModel initilize I pass two args groupId and type which should be received by savedStateHandle. It was working absolutely fine for 1.4.0 fragment lib version; however, in this version I only get groupId.
If there a bug in lib itself?
As I said, 1.5.5 fragment lib version have this issue, older version like 1.4.0 working fine and I get all arguments passed in bundle, well received by viewModel.

Related

What is the best way to handle Android ViewModels in navigation graph scope with a Fragment shared across multiple sub-graphs?

It is reasonable that a Fragment might be used from several sub-graphs in the navigation hierarchy. In this case if the fragment depends on a view model provided by the parent Fragment the view model needs to be in a sub-graph scope that changes depending on its parent.
Kotlin provides a convenient way to get a graph scoped view model:
private val fvm: SoftenerViewModel by navGraphViewModels(R.id.navigation_graph_softener)
but this hard codes in the sub-graph id.
What is the best way to address this case?
One approach is the following, but given that the new extension function is not already in the library it raises the question if there is a better way?
By direct analogy with the Kotlin supplied extension function define the following extension:
/**
* Derived from [androidx.navigation.navGraphViewModels]
*/
#MainThread
inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
viewModelArgKey: String,
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> {
val backStackEntry by lazy {
val id = arguments?.getInt(viewModelArgKey, 0)?:0
require(id != 0) {"Fragment argument $viewModelArgKey required."}
findNavController().getBackStackEntry(id)
}
val storeProducer: () -> ViewModelStore = {
backStackEntry.viewModelStore
}
return createViewModelLazy(VM::class, storeProducer, {
factoryProducer?.invoke() ?: backStackEntry.defaultViewModelProviderFactory
})
}
This differs only in that the argument is a String, and the name of the argument is what needs to be given. The laziness ensures that the fragment arguments are referenced at the right time.
It is used just like the existing androidx library extension:
private val fvm: SoftenerViewModel by navGraphViewModels("view_model_id")
The only requirement is the navigation sub-graph defines the argument:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation"
app:startDestination="#+id/navigation_home"
>
<fragment
android:id="#+id/navigation_home"
android:name="com...HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home"
/>
<navigation
android:id="#+id/navigation_graph_softener"
android:label="Softener"
app:startDestination="#id/navigation_softener"
>
<fragment
android:id="#+id/navigation_softener"
android:name="com.hanafey.android.waterstats.ui.softener.SoftenerFragment"
android:label="#string/title_softener"
tools:layout="#layout/fragment_softener"
/>
<fragment
android:id="#+id/navigation_softener_history"
android:name="com...SoftenerHistoryFragment"
android:label="Softener History"
tools:layout="#layout/fragment_softener_history"
>
<argument
android:name="view_model_id"
app:argType="reference"
android:defaultValue="#id/navigation_graph_softener"
/>
</fragment>
</navigation>
</navigation>

safeargs argument not found in NavDirections

I have implemented an argument to be passed between fragments in nav_graph, however when I attempt to set the argument in the originating fragment, the argument is not found by the NavDirections.
Note that Navigation works fine before trying to pass the argument.
If I do a Clean Project I lose the NavDirections. If I do a Rebuild I lose the argument.
Gradle:app
//Navigation
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
apply plugin: "androidx.navigation.safeargs.kotlin"
nav_graph.xml
<fragment
android:id="#+id/destination_home"
android:name="com.android.joncb.flightlogbook.HomeFragment"
android:label="#string/lblHome"
tools:layout="#layout/fragment_home">
<action
android:id="#+id/action_home_to_fltHistory"
app:destination="#id/destination_fltHistory" />
<action
android:id="#+id/action_home_to_stats"
app:destination="#id/destination_statistics" />
<action
android:id="#+id/action_home_to_newFlight"
app:destination="#id/destination_newFlight" />
<action
android:id="#+id/action_home_to_fltDetails"
app:destination="#id/destination_fltDetails" />
<argument
android:name="fltData"
app:argType="string" />
</fragment>
and in my Home Fragment I get the error "Unresolved reference: fltData"
card_nextFlight.setOnClickListener {
val actionDetails = HomeFragmentDirections.actionHomeToFltDetails()
actionDetails.fltData ( flightData.toString())
Navigation.findNavController(it).navigate(actionDetails)
}
flightData is a data class
data class FlightDTO(
var airlineName: String, var faCode: String, var fltNo: String, var aircraft: String,
var depAP: String, var arrAP: String, var schedDep: String, var schedArr: String,
var date: String, var leg: Int = 0, var actDep: String = "", var actArr: String = "" ){
...
override fun toString(): String {
return "$airlineName $faCode $fltNo $aircraft $depAP $schedDep $arrAP $schedDep $date"
}
}
I want to pass the class ideally by making the class Parcelable, but until I can pass a string, there is no point venturing down the parcel line.
You are writing your XML wrong, think like this : The way I structure my XML properties is the way the generated code will look like and received between destinations sort of...
So basically in your nav_graph.xml you should change to:
<fragment
android:id="#+id/destination_home"
android:name="com.android.joncb.flightlogbook.HomeFragment"
android:label="#string/lblHome"
tools:layout="#layout/fragment_home">
<action
android:id="#+id/action_home_to_fltHistory"
app:destination="#id/destination_fltHistory" />
<action
android:id="#+id/action_home_to_stats"
app:destination="#id/destination_statistics" />
<action
android:id="#+id/action_home_to_newFlight"
app:destination="#id/destination_newFlight" />
<action
android:id="#+id/action_home_to_fltDetails"
app:destination="#id/destination_fltDetails">
<argument
android:name="fltData"
app:argType="string" />
</action>
</fragment>
and in your destination it should look something like:
<fragment
android:id="#+id/destination_fltDetails"
android:name="com.android.joncb.flightlogbook.FlightDetailsFragment"
android:label="#string/lblFlightDetails"
tools:layout="#layout/fragment_flight_details">
<argument
android:name="fltData"
app:argType="string" />
</fragment>
and in your flight details fragment the properties are received by using:
private val args: FlightDetailsFragmentArgs by navArgs()
println(args.fltData) // prints the navigation data
UPDATE:
Forgot to mention your OnClickListener in your Home fragment that would look more like this:
card_nextFlight.setOnClickListener {
val actionDetails = HomeFragmentDirections.actionHomeToFltDetails(flightData.toString())
Navigation.findNavController(it).navigate(actionDetails)
}
For my case, I wrote a buggy code like that -
NavController navController = NavHostFragment.findNavController(this);
NavDirections navDirections = MyDestinationFragmentDirections.actionMyAction(myArgumentValue);
navController.navigate(navDirections.getActionId());
Then I change the last line into this -
navController.navigate(navDirections);
And finally,it worked as expected!!!
The logic behind this was, in NavController class the method which accepting int (resId of action) always put null argument -
public void navigate(#IdRes int resId) {
navigate(resId, null);
}
So we should use -
public void navigate(#NonNull NavDirections directions) {
navigate(directions.getActionId(), directions.getArguments());
}
method if we are willing to pass an arguments via an action.
my mistake was the following. I had something like
NavDirections action =
SpecifyAmountFragmentDirections
.actionSpecifyAmountFragmentToConfirmationFragment();
I changed to something like
ConfirmationAction action =
SpecifyAmountFragmentDirections
.actionSpecifyAmountFragmentToConfirmationFragment();
Rather than pass a data class, I have created a JSON String and passed a string
card_nextFlight.setOnClickListener {
val dataString = flightData.toJSONString()
val actionDetails = HomeFragmentDirections.actionHomeToFltDetails(dataString)
Navigation.findNavController(it).navigate(actionDetails)
}
To get this to work I had to modify the actionHomeToFltDetails function to receive a string in HomeFragmentsDirections
fun actionHomeToFltDetails(fltData: String): NavDirections = ActionHomeToFltDetails(fltData)
}
I could not get #Lucho approach to handle the arg in the destination fragment to work so reverted to bundle management, and converted the JSON string back to a data class
const val ARG_PARAM1 = "fltData"
.
.
.
arguments?.let {
argFltData = it.getString(ARG_PARAM1)
Log.e("args","Passed Argument: $argFltData")
fltData = gson.fromJson(argFltData, FlightDTO::class.java)
}
Thanks again for your input and I hope this helps someone else through the drama.

Exception Inflating nav_graph when passing Parcelable object between Navigation Fragments

I'm attempting to pass an object between navigation fragments. I'm able to build the project, but when it launches, I get an error on the nav_graph stating: "Exception inflating nav_graph line 20". Line 20 is the argument line on the nav_graph. I just added the #Parcelize keyword to the top of the class I'm trying to pass and setup the nav_graph. Do I need to do something else?
Team Class:
#Parcelize
public class Team {
#SerializedName("idTeam")
#Expose
private String idTeam;
#SerializedName("idSoccerXML")
#Expose
private String idSoccerXML;
#SerializedName("idAPIfootball")
#Expose
private String idAPIfootball;
#SerializedName("intLoved")
#Expose
private String intLoved;
#SerializedName("strTeam")
#Expose
private String strTeam;
#SerializedName("strTeamShort")
#Expose
private String strTeamShort;
Nav_Graph:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="com.jaykallen.searchapi.ui.HomeFragment"
android:label="HomeFragment">
<action
android:id="#+id/action_homeFragment_to_resultsFragment"
app:destination="#id/resultsFragment" />
</fragment>
<fragment
android:id="#+id/resultsFragment"
android:name="com.jaykallen.searchapi.ui.ResultsFragment"
android:label="ResultsFragment">
<argument
android:name="choice"
app:argType="com.jaykallen.searchapi.model.Team"
app:nullable="true" />
</fragment>
</navigation>
HomeFragment Method:
private fun choiceClicked(chosen: Team) {
println("User clicked: ${chosen.strTeam}")
homeViewModel.choice = chosen
val action = HomeFragmentDirections.actionHomeFragmentToResultsFragment(chosen)
Navigation.findNavController(view!!).navigate(action)
}
ResultsFragment Method:
private fun getSafeArgs() {
arguments?.let {
val args = ResultsFragmentArgs.fromBundle(it)
teamChosen = args.choice
if (teamChosen != null) {
println("Safe Argument Received=${teamChosen?.strTeam}")
updateUi(teamChosen)
}
}
}
It turns out, all you needed to do is implement the Parcelable interface on your Java object. Normally, if you were using Kotlin, theĀ #Parcelize annotation wouldn't have allowed you to compile without the Parcelable interface. It seems this compile time protection doesn't work for Java code.
By using Java objects, you'll also lose out on all of the automatic code generation goodies that come with the #Parcelize annotation.
In other words, I recommend you convert your Java file to Kotlin if you would like to take advantage of the #Parcelize annotation.

Jetpack Navigation deeplink with query parameters

it looks like it isn't possible to process a deeplink with query parameters in the new Jetpack Navigation library. If you put the following to the navigation.xml:
<deepLink app:uri="scheme://host/path?query1={query_value}" /> then the deeplink does not open the fragment.
After some digging I found that the culprit is probably in the NavDeepLink when it transforms the url from xml to a Pattern regex. Looks like the problem is a question mark that is not excaped.
I wrote a test which fails:
#Test
fun test() {
val navDeepLink = NavDeepLink("scheme://host/path?query1={query_value}")
val deepLink = Uri.parse("scheme://host/path?query1=foo_bar")
assertEquals(true, navDeepLink.matches(deepLink))
}
To make the test pass all I have to do is to escape the ? as following:
#Test
fun test() {
val navDeepLink = NavDeepLink("scheme://host/path\\?query1={query_value}")
val deepLink = Uri.parse("scheme://host/path?query1=foo_bar")
assertEquals(true, navDeepLink.matches(deepLink))
}
Am I missing something really basic here to pass query values to my Fragment or is this not supported feature at the moment?
You need to add DeepLink Navigation to AndroidManifest.xml ( special Activity that handles the fragment) so when deeplink clicked your app can receive the DeepLink and pass it to that navigation and fragment & can read it as argument:
I'll put Kotlin codes here :
In your navigation file, your fragment that gonna handle the deeplink with arguements must be like this:
<fragment
android:id="#+id/menu"
android:name="ir.hamplus.fragments.MainFragment"
android:label="MainFragment">
<action android:id="#+id/action_menu_to_frg_messenger_main"
app:destination="#id/frg_messenger_main"/>
<deepLink app:uri="http://hamplus.ir/request/?key={key}&id={id}" />
<argument android:name="key" app:argType="string"/>
<argument android:name="id" app:argType="string"/>
</fragment>
read deeplink arguments in frasgment /Activity :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Or in activity read the intent?.data
arguments?.let {
Log.i("TAG", "Argument=$arguments")
var key = it.getString("key")
Log.i("TAG", "key=$key")
var id = it.getString("id")
Log.i("TAG", "id=$id")
}
}
Also add the nav-graph on AndroidManifest.xml in related Activity :
<activity
android:name=".MainActivity"
android:theme="#style/AppTheme.NoActionBar" >
<nav-graph android:value="#navigation/main_navigation"/>
</activity>
package androidx.navigation
import android.net.Uri
import androidx.test.runner.AndroidJUnit4
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
#RunWith(AndroidJUnit4::class)
class NavTest {
#Test
fun test() {
val navDeepLink = NavDeepLink("scheme://host/path\\?query1={query_value1}&query2={query_value2}")
val deepLink = Uri.parse("scheme://host/path?query1=foo_bar&query2=baz")
val bundle = navDeepLink.getMatchingArguments(deepLink)!!
assertTrue(bundle.get("query_value1") == "foo_bar")
assertTrue(bundle.get("query_value2") == "baz")
}
}
In the end it looks like NavDeepLink treats non escaped as "?" match-zero-or-one quantifier. You need to escape it. In other words, we have a leak of non documented implementation detail.
It might be not related to the exactly this case, but there is some similar issues with escaping "&" with "\" when using add command.
The issue was also touched in the following channel.

How to pass object of type Parcelable to a Fragment using Navigation type safeargs plugin

I am rewriting my simple UI app to use Navigation architecture component, I need to pass a Pojo that implements Parcelable, have not seen any doc on how to do that.
Any help would be appreciated.
Since safe-args-gradle-plugin:1.0.0-alpha03 you can use Parcelable objects by using their fully qualified class name:
<argument
android:name="item"
app:argType="com.example.app.model.Item"/>
Parcelable arguments are now supported, using a fully qualified class name for app:type. The only default value supported is "#null" (https://issuetracker.google.com/issues/79563966)
Source: https://developer.android.com/jetpack/docs/release-notes
To support nullability one has to use android:defaultValue="#null" with app:nullable="true".
I know the answer is already there but this may help someone. Code snippet
In build.gradle add this dependancy
ext{
...
navigation_version = '1.0.0-alpha11'
}
dependencies {
...
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
}
In app/build.gradle
apply plugin: 'androidx.navigation.safeargs'
...
In Navigation graph
<fragment
android:id="#+id/source_fragment_id"
android:name="app.test.SourceFragment"
android:label="#string/source_fragment_label"
tools:layout="#layout/source_fragment_layout">
<action
android:id="#+id/action_source_fragment_to_destination_fragment"
app:destination="#id/destination_fragment_id"
...
/>
</fragment>
<fragment
android:id="#+id/destination_fragment_id"
android:name="app.test.DestinationFragment"
android:label="#string/destination_fragment_label"
tools:layout="#layout/destination_fragment_layout">
<argument
android:name="variableName"
app:argType="app.test.data.model.CustomModel" />
...
</fragment>
Note: CustomModel should be Parcelable or Serializable.
When navigating to this DestinationFragment from SourceFragment
val direction = SourceFragmentDirections.ActionSourceFragmentToDestinationFragment(customModel)
findNavController().navigate(direction)
Now retrieving the value from bundle in DestinationFragment
...
import app.test.DestinationFragmentArgs.fromBundle
class DestinationFragment : Fragment() {
val variableName by lazy {
fromBundle(arguments!!).variableName
}
...
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Log.e(DESTINATION_FRAGMENT_TAG,"onCreateView")
//Can use CustomModel variable here i.e. variableName
}
}
Right now you can't use safe args with types apart from integer, string, inferred and reference, there's an issue opened asking for other types.
What you can do now is to normally pass a bundle when using the navigate() method to navigate to a destination:
var bundle = bundleOf("amount" to amount)
view.findNavController().navigate(R.id.confirmationAction, bundle)
And you can use the usual getArguments (or just arguments in kotlin) to retrieve that:
val tv = view.findViewById(R.id.textViewAmount)
tv.text = arguments.getString("amount")
You can use boolean, reference, integer, long, string, enum, parcelable and even serializable. But forget about the last one ;-)
Better use the latest plugin version safe-args-gradle-plugin:1.0.0-alpha08 and specify the fully qualified classname:
<fragment
...>
<argument
android:name="data"
app:argType="com.example.ParcelableData" />
</fragment>
from your
package com.example
data class ParcelableData(val content: String) : Parcelable { ... }
And you can send arrays of all the argTypes:
<argument
android:name="data"
app:argType="string[]" />

Categories

Resources