I have an activity which has a NavHostFragment. The activity receives certain values in its intent. I want to pass this data to the first fragment i.e startDestination of the navigation graph. I couldn't find any documentation regarding this.
I have already gone through this question on SO but I can't seem to find the addDefaultArguments method for navController.getGraph().
Is it possible to pass bundle to startDestination?
Answering my own question as I found the correct approach in the updated Navigation documentation.
At the time of writing this answer, I am using Navigation 2.2.0-alpha01
If you want to pass some data to the start destination directly as arguments from host activity, you need to manually set your host’s navigation graph inside the host activity’s onCreate() method, as shown below:
Get you navController:
val navController by lazy { findNavController(R.id.<your_nav_host_id>) }
Then in the host activity's onCreate()
val bundle = Bundle()
bundle.putString("some_argument", "some_value")
navController.setGraph(R.navigation.<you_nav_graph_xml>, bundle)
Or if you want to pass the whole intent extras as it is to the startDestination:
navController.setGraph(R.navigation.<you_nav_graph_xml>, intent.extras)
Since intent.extras would return a Bundle only
When you are setting the navGraph using setGraph() method, you should avoid setting the app:NavGraph attribute in
the NavHostFragment definition, because doing so results in inflating
and setting the navigation graph twice.
While reading these arguments in your startDestination fragment:
If you are using the Safe Args Plugin (which is very much recommended), then in your fragment:
private val args by navArgs<DummyFragmentArgs>()
Safe Args plugin would generate an Args class by appending Args to your fragment name. For example, if you fragment is called DummyFragment then Safe Args would generate a class called DummyFragmentArgs
where navArgs<> is an extension function defined in Android KTX
If you are not using Android KTX, you can get the args object like:
val args = DummyFragmentArgs.fromBundle(arguments!!)
Once you've acquired the arguments object, you can simply fetch your arguments:
args.someArgument
Notice how we passed "some_argument" as argument, and we are reading it as someArgument using Safe Args
If you are not using Safe Args (there is no reason to not use it though), you can access your arguments like this:
arguments?.getString("some_argument")
All of this is documented in Migrate to Navigation Component documentation here:
https://developer.android.com/guide/navigation/navigation-migrate#pass_activity_destination_args_to_a_start_destination_fragment
I found the solution after some research. It works with the latest Navigation library release. Refer the below code:
Add this in your activity layout. Note: We are not setting app:navGraph argument in the xml file. We will set it dynamically.
<fragment
android:id="#+id/fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true" />
In your activity java file, write the below code and make changes accordingly. Use NavArgument to pass your argument value and add the argument to your custom Navgraph and then set graph.
public class YourActivity extends AppCompatActivity {
private NavArgument nameArg, mailArg;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.profile);
nameArg = new NavArgument.Builder().setDefaultValue("your name").build();
mailArg = new NavArgument.Builder().setDefaultValue("your email id").build();
NavController navController = Navigation.findNavController(this, R.id.fragment);
NavInflater navInflater = navController.getNavInflater();
NavGraph navGraph = navInflater.inflate(R.navigation.nav_profile_graph);
navGraph.addArgument("your name key", nameArg);
navGraph.addArgument("your mail key", mailArg);
navController.setGraph(navGraph);
}
}
Write the navigation graph below and add the same argument keys to the starting fragment.
<?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/nav_graph"
app:startDestination="#+id/profile_basic">
<fragment
android:id="#+id/profile_basic"
android:name="com.yourpackage.ProfileBasicFragment"
android:label="Profile Basic"
tools:layout="#layout/fragment_profile_basic">
<argument android:name="your name key"
app:argType="string"/>
<argument android:name="your mail key"
app:argType="string"/>
</fragment>
</navigation>
In your fragment, just fetch the values using getArguments() function.
String name = getArguments().getString("your name key");
String mail = getArguments().getString("your mail key");
i also came across same issue,
This is how i resolved it:
Remove the the xml setup of NavHostFragment from your_activity.xml : i.e remove app:navGraph="#navigation/nav_graph
This is how your XML Should look like.
<fragment
android:id="#+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
/>
Add Setup for NavHostFragment Programatically in onCreate() of activity.
And pass bundle data using NavGraph.addDefaultArguments(bundleData) api
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.editor_layout)
setupNavigation()
}
private fun setupNavigation() {
val navHostFragment = nav_host as NavHostFragment
val navController = navHostFragment.navController
val navInflater = navController.navInflater
val graph = navInflater.inflate(R.navigation.nav_graph)
graph.addDefaultArguments(intent!!.extras!!) // This is where you pass the bundle data from Activity to StartDestination
navHostFragment.navController.graph = graph
}
UPDATE:
Dependencies in my Project Gradle file:
dependencies {
def nav_version = "1.0.0-alpha08"
implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin
implementation "android.arch.navigation:navigation-ui:$nav_version" // use -ktx for Kotlin}
}
NOTE: In Navigation Component version 1.0.0-alpha09 for some reason google have no method as addDefaultArguments() might be fixed soon. But lower version's have addDefaultArguments() method.I have checked both in java and kotlin so try using 1.0.0-alpha07 or 1.0.0-alpha08
I checked the source code an saw that there a lot of changes regarding navigation destination and arguments.
I think the proper way to pass arguments to start destination is using 'addArgument' method, something like this:
val argument1 = 1 //First value
val argument2 = "Value" //Second value
val navArgument1=NavArgument.Builder().setDefaultValue(argument1).build()
val navArgument2=NavArgument.Builder().setDefaultValue(argument2).build()
navController.getGraph().addArgument("Key1",navArgument1)
navController.getGraph().addArgument("Key2",navArgument2)
Maybe there is a better way, but i didn't found one.
Though it is a late answer there is a very easy way for passing bundles using navigation controller/Navigation Architecture:
Fragment that will send data
//Best to put in onViewCreated
val navController = findNavController()
//Put in code where you want to start navigation
val b = Bundle()
b.putString("key_1", "Data 1")
b.putString("key_2", "Data 2")
navController.navigate(R.id.action_signupFragment_to_signinFragment,b)
Fragment that will receive data
val data_1: String = arguments?.getString("key_1")?:""
val data_1: String = arguments?.getString("key_2")?:""
Depending on the type of data you are sending you need to update the safe call or make the variables nullable.
I cannot find this method, too. It is not existant in the architecture components documentation.
But the is another way to set arguments to the start destination:
// Kotlin Code, in Fragment
with(findNavController().graph) {
get(startDestination).addArgument(...)
}
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = container as NavHostFragment
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(R.navigation.main_nav)
var data : Data = intent.getParcelableExtra("DATA") as Data
var bundle : Bundle = Bundle()
bundle.putParcelable("DATA", data)
graph.addDefaultArguments(bundle)
graph.addDefaultArguments(intent!!.extras!!)
navHostFragment.navController.graph = graph
}
}
Add the above code in Activity for sending the data using using navigation
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
var data = NavHostFragment.findNavController(this).graph.defaultArguments.getParcelable("DATA") as Booking
}
Add the above code in fragment side
you can add arguments in your graph like this
<fragment
android:id="#+id/exampleFragment"
android:label="example_fragment"
android:name="com.example.yourapp.ui.ExampleFragment"
tools:layout="#layout/example_fragment">
<argument
android:name="exampleArgs"
app:argType="reference"
android:defaultValue="#string/example"/>
</fragment>
https://developer.android.com/guide/navigation/navigation-pass-data
Navigation 1.0.0
val navHostFragment = root_nav_host_fragment as NavHostFragment
val navController = navHostFragment.navController
val navInflater = navController.navInflater
val graph = navInflater.inflate(R.navigation.navigation)
val sectionId = intent.getIntExtra(KEY_SECTION_ID, -1)
val bundle = bundleOf(KEY_SECTION_ID to sectionId)
navHostFragment.navController.setGraph(graph, bundle)
Related
Hi I have a adapter which displays the list of items and all the functions related to setItems, getCount, notifyDataSetChanged.
adapter also has calls to api's through use cases.
Structure is
Adapter -> UseCase -> Repository -> apiLayer
I am aware that fragemnts and activities should not contains calls to api (usecases in my instance).
So should adapter's have api calls (usecases in my instance)
Thanks
R
It is possible, but from a software design point of view I would not recommend it.
The Adapter's responsibility is to connect your Data with the View and make it available to the ListView/RecyclerView. The Adapter should not have any other dependency (knowledge). This will also make it more robust to changes.
So you should consider that only the Activity/Fragment talks to your Presenter and delegates the results from the Presenter to the Adapter.
This will also make (unit) testing (of the Presenter) more easier.
class YourActivity: Activity() {
private val presenter: YourPresenter = // ...
override fun onCreate() {
val adapter: YourAdapter = YourAdapter()
recyclerView.setAdapter(adapter)
val data = presenter.getData()
adapter.submit(data)
}
}
class YourPresenter(private val useCase: UseCase : Presenter() {
fun getData(): List<Data> {
return useCase.fetchData()
}
}
With Android data-binding framework, I understand that you can pass an object that extends baseObservable to the layout xml, use #Bindable on getters and do notifyPropertyChanged(BR.xxx) to have the related part re-evaluated.
What I don't understand is this: if you don't use most the stuff above and just call the getter directly in xml, when would it be evaluated?
Here's the code:
my_widget.xml
<layout 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">
<data>
<variable
name="someViewModel"
type="com.example.SomeViewModel" />
</data>
<androidx.cardview.widget.CardView>
<View
android:id="#+id/testView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="#{someViewModel.getName() ? View.VISIBLE : View.GONE}" />
</androidx.cardview.widget.CardView>
</layout>
MyView.java
MyWidgetBinding binding = MyWidgetBinding.inflate(LayoutInflater.from(mContext), parent, false);
binding.setSomeViewModel(someViewModel);
Questions:
If someViewModel.name ever changes, does the testView's visibility refreshes?
When does someViewModel.getName() get evaluated or how often?
If the expression is more complicated, something like:
android:visibility="#{func(otherVariable, someViewModel.getName()) ? View.VISIBLE : View.GONE}", say otherVariable is another variable defined in data section above, if somehow otherVariable gets re-set, then someViewModel.getName() will get evaluated and testView will reflect the latest visibility value, correct?
Following up on question 3, if otherVariable is changed to otherVariable.a where a is a 'bindable' field and notifyPropertyChanged(BR.a) is called in otherVariable then someViewModel.getName() will also get re-evaluated and testView will reflect the latest visibility value, correct?
Additionally, if I re-set someViewModel by calling binding.setSomeViewModel() but pass in the SAME someViewModel instance, does it do anything? Will the expression get re-evaluated?
I suggest you to create custom binding adapter for mutable visibility and use LiveData to update the visibility.
Code :
#BindingAdapter("mutableVisibility")
fun setMutableVisibility(view: View, visibility: MutableLiveData<Boolean>) {
val parentActivity: AppCompatActivity? = view.getParentActivity()
if (parentActivity != null) {
visibility.observe(
parentActivity,
Observer { value -> if (value) view.visibility = View.VISIBLE
else view.visibility = View.GONE})
}
}
To get the parent activity create ActivityExtensions.kt file and add the following function in it:
fun View.getParentActivity(): AppCompatActivity?{
var context = this.context
while (context is ContextWrapper) {
if (context is AppCompatActivity) {
return context
}
context = context.baseContext
}
return null
}
And in the ViewModel :
//Other code here...
val itemVisibility = MutableLiveData<Boolean>()
//Other logic here to init itemVisible
if(itemVisibile) itemVisibility.value = true else itemVisibility.value = false
And finaly the layoutItem :
<View
android:id="#+id/testView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:mutableVisibility ="#{viewModel.itemVisibility}" />
If someViewModel.name ever changes, does the testView's visibility
refreshes?
it depends on the underlying technology you are using. LiveData? Yes BaseObservable you have to manually notify that the observed property changed.
When does someViewModel.getName() get evaluated or how often?
LiveData ? when you set/post a value. BaseObservable when you notify it
Same as point 2
If you change the underlying value and notify this change properly, it will get propagated accordingly. If you change the instance of the observed object it will not.
My Activity:
class PlayerDetails : AppCompatActivity(), View.OnClickListener {
private lateinit var binding: ActivityPlayerDetailsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_player_details)
}
I'm trying to understand how the data binding process works, this is how I understand it:
private lateinit var binding: ActivityPlayerDetailsBinding
instantiates the ViewDataBinding object.
binding = DataBindingUtil.setContentView(this,
R.layout.activity_player_details) is in 2 parts:
DataBindingUtil.setContentView(this,
R.layout.activity_player_details) sets the content view to the given layout
It then returns the binding object to the binding variable (binding = ...) which can then be used to access views in the layout.
Is this an accurate way of describing how the code is working? I found the source code for DataBindingUtil.java hard to understand. Mostly because setContentView() is being called even though it appears to be assigned instead binding = ....
DataBindingUtil.setContentView(this, R.layout.activity_player_datails) do almost same thing to return binding object. Although DataBindingUtils.setContentView call activity.setContentView before returning.
Instead, I usually override setContentView to make sure assign binding object into variables and sets content to the given layout.
override fun setContentView(layoutResID: Int) {
binding = DataBindingUtil.inflate(LayoutInflater.from(context), layoutResID, null, false)
super.setContentView(mBinding.root)
}
I used the anko library to create a login view.
class SingInView : AnkoComponent<SingleInActivity> {
override fun createView(ui: AnkoContext<SingleInActivity>) = with(ui) {
verticalLayout {
lparams(width = matchParent, height = matchParent)
textView("Member Login")
editText {
hint = "E-mail"
}
editText {
hint = "PassWord"
}
button("Login")
}
}
}
and SingleInActivity.kt
class SingleInActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState)
SingInView().setContentView(this)
and MainActivity.java
public class MainActivity extends AppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startActivity(new Intent(this, SingInView.class));
finish();
}
}
current My app MainActivity -> SingleInActivity -> SingInView .
of course it can be made simply.
but there is a condition
1. MainActivity is java (kotlin prohibition)
2. use only MainActivity, SingInView.
How to solve this problem?
How to call the Anko class directly from a Java class
If you dig through the Anko source code you'll quickly find this:
interface AnkoComponent<in T> {
fun createView(ui: AnkoContext<T>): View
}
And from the wiki (where MyActivityUI is the component): MyActivityUI().setContentView(this). Now, the AnkoComponent is just an interface and the setContentView method is an extension function that returns createView.
Anyways, the setContentView extension function passes the last variable of the AnkoContextImpl as true. The last variable is whether or not to actually set the content view, which is the reason the activity is passed in the first place.
TL;DR (and possibly more sensible summary of my point):
The component is not an Activity
The setContentView method is not a replacement for setContentView in an Activity; just a wrapper for it.
And since it isn't an activity, you can't use an intent into it. And, as a result of that, you cannot use it standalone. You need an activity. Now, you can of course use the regular approach, but there's also another way. Since the AnkoComponent itself doesn't have any fields, it can be serialized without much trouble. Just to clarify: some fields can be serialized even if it isn't serializable (all though some classes like Context cannot be serialized). Anyways, you create an activity:
class AnkoComponentActivity : AppCompatActivity(){//Can be a regular Activity too
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState);
val component = intent.getSerializableExtra("uiComponent") as AnkoComponent<AnkoComponentActivity>//The type has to match this activity, or setContentView won't allow passing `this`
component.setContentView(this)//The context of the activity doesn't get passed until this point, which enables the use of this system.
}
}
Or it's equivalent in Java:
public class AnkoComponentActivity extends AppCompatActivity {
public void onCreate(Bundle sis){
super.onCreate(sis);
AnkoComponent<AnkoComponentActivity> component = (AnkoComponent<AnkoComponentActivity>) getIntent().getSerializableExtra("uiComponent");
org.jetbrains.anko.AnkoContextKt.setContentView(component, this);//For reference, this is how you call Kotlin extension functions from Java
}
}
Note that any UI component sent to this class has to be declared with <AnkoComponentActivity>. In addition, the components have to implement Serializable. Otherwise they can't be passed through the Bundle. Alternatively, you can use ints or Strings as identifiers and use the value to pick which AnkoComponent to show.
All though, the absolutely easiest way is just creating one activity per component.
TL;DR: AnkoComponent is not an Activity, meaning you can't use intents into it. You have to use an Activity, but using Serializable enables you to pass the component through a bundle to an Activity made for manual creation of multiple AnkoComponents without specifying specific types.
I have a Java Android application which I want to change to Scala. I have many fragments and I want to know what is the best way to do this in Scala.
This is my Java fragment class MyFragment:
public class MyFragment extends Fragment {
private WebView myWebView;
private TextView myTextView;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View myView = inflater.inflate(R.layout.my_view, container, false);
myWebView = (WebView) myView.findViewById(R.id.my_webview);
myWebView.loadUrl("http://www.google.com/");
myTextView = (TextView) myView.findViewById(R.id.my_textview);
myTextView.setText("Google.com");
return myView;
}
}
I always have this base structure: some private UI elements which I instantiate in onCreateView, do some things and return the view (Not shown here: in other on* methods I also do some actions with the UI elements).
I found some articles which do a lazy val like described here: http://blog.andresteingress.com/2011/09/20/programming-android-with-scala/
But in my case, this does not work, because I have fragments and not activities. First I have to inflate the main View myView and then I can get the UI elements of it.
What is the best way to do this in Scala?
--- UPDATE ---
My Scala code looks like this at the moment:
class MyFragment extends Fragment {
private var myTextView: TextView = null
override def onCreateView(inflater: LayoutInflater,
container: ViewGroup, savedInstanceState: Bundle): View = {
val myView = inflater.inflate(R.layout.my_view, container, false)
val myWebView = myView.findViewById(R.id.my_webview).asInstanceOf[WebView]
myWebView.loadUrl("http://www.google.com/")
myTextView = myView.findViewById(R.id.my_textview).asInstanceOf[TextView]
myTextView.setText("Google.com")
myView
}
}
So, what can I improve here? myTextView is a private var because I have to access it several methods in this Fragment. It seems I can not do the stuff explained here: http://blog.andresteingress.com/2011/09/20/programming-android-with-scala/ with lazy val TypedActivity and the implicit conversion of OnClickListener, because I use fragments. So how can I get rid of boilerplate code with .asInstanceOf[T] and make it more Scala like?
Based on your updated code I can only make some suggestion to be more "scala-ish"
Use Option instead of null for your members
private var myWebView: Option[WebView] = None
private var myTextView: Option[TextView] = None
To avoid explicit casting of your views in the code, you need to move it elsewhere, but you cant' get rid of it, because the original android API doesn't give you any clue as to the runtime or compiletime type of the returned objects. To overcome this issue, the post you mentioned uses custom-made typed resources and a trait that handles the types from these.
case class TypedResource[T](id: Int)
object TR {
object id {
val my_webview = TypedResource[TextView](R.id.my_webview)
val my_textview = TypedResource[WebView](R.id.my_textview)
//... you must put here all your typed views referenced by id
}
}
trait TypedViewHolder {
def view: View
//the method explicitly casts to the needed resource type based on the argument
def findView[T](tr: TypedResource[T]): T = view.findViewById(tr.id).asInstanceOf[T]
}
object TypedResource {
//this will implicitly convert your views to a corresponding TypedViewHolder
//this lets you avoid explicit type cast to get your view
implicit def view2typed(v: View): TypedViewHolder = new TypedViewHolder { def view = v }
}
Now we can use the above code
val myView = inflater.inflate(R.layout.my_view, container, false)
val myWebView = myView.findView(TR.id.my_webview)
myWebView.loadUrl("http://www.google.com/")
val myTextView = myView.findView(TR.id.my_textview)
myTextView.setText("Google.com")
Putting both things together
class MyFragment extends Fragment {
private var myWebView: Option[WebView] = None
private var myTextView: Option[TextView] = None
override def onCreateView(
inflater: LayoutInflater,
container: ViewGroup,
savedInstanceState: Bundle): View = {
//imports the implicit conversion
import TypedResource._
val myView = inflater.inflate(R.layout.my_view, container, false)
myWebView = Some(myView.findView(TR.id.my_webview))
//now we're using options, so we must call methods on the inner value
//we can use Option.map(...) to do it [see http://www.scala-lang.org/api/current/index.html#scala.Option]
myWebView.map(_.loadUrl("http://www.google.com/"))
myTextView = Some(myView.findView(TR.id.my_textview))
//same as above
myTextView.map(_.setText("Google.com"))
myView
}
}
I hope this helps you out. I'm no expert with android so I can only get so far.
Why don't you simply write the fragment in Scala without bothering about "the best way to do this in Scala"? There might be none.
I'd start with removing public from the class definition and including the other goodies - TypedActivity from the article - in the activity. Then, set up the development environment - the IDE - and run the application. If it works, you're done (with the very first step in the migration). I don't think you need lazy val's from the very beginning.
Do small steps so the migration's easier.