This bounty has ended. Answers to this question are eligible for a +50 reputation bounty. Bounty grace period ends in 13 hours.
jack_the_beast is looking for a canonical answer.
I'm sorry for posting a question on the subject again but this is driving me crazy.
I have a fragment and I'm adding views programmatically to a LinearLayout:
<LinearLayout
android:id="#+id/items_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
val items =buildView1(event) +buildView2(event) +buildCrew(event)
items.forEach {dataBinding.itemsContainer.addView(it)}
crew is a fragments, so I add it like this:
private fun buildCrew(event: Event): List<View> {
return listOf(
FrameLayout(requireContext()).apply {
id = View.generateViewId()
val transaction = childFragmentManager.beginTransaction()
val f = CrewFragment()
transaction.add(id, f)
transaction.commit()
})
}
it works but navigating away from this page and coming back results in the error:
No view found for id 0x1 (unknown) for fragment CrewFragment{92ba272}
I also tried to create a custom view with a FragmentContainerView:
<androidx.fragment.app.FragmentContainerView
android:id="#+id/event_detail_crew"
android:name="redacted.package.CrewFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
this solves the error but causes two problems:
the fragment seemed to be attached using the activity's fragment manager, I need the containing fragment's childFragmentManager as the two need to share a viewModel with
private val viewModel: CrewViewModel by lazy {
requireParentFragment().getViewModel()
}
when navigating away and coming back the contents of CrewFragment are not visible anymore, none of CrewFragment's lifecycle methods are called
What is going on here?
Related
I am trying to get a view model in two places, one in the MainActivity using:
val viewModel:MyViewModel by viewModels()
The Other place is inside a compose function using:
val viewModel:MyViewModel = hiltViewModel()
When I debug, it seems that those are two different objects. Is there anyway where I can get the same object in two places ?
Even though you solved your issue without needing the view model, the question remained unanswered so I am posting this in case someone else finds it helpful.
This answer explains how view model scopes are shared and how you can override it.
In case you are using Navigation component, this should help. However, if you don't want to pass down view models or override the provided ViewModelStoreOwners, you can access the parent activity's view model in any child composable like below.
val composeView = LocalView.current
val activityViewModel = composeView.findViewTreeViewModelStoreOwner()?.let {
hiltViewModel<MyViewModel>(it)
}
I have a ViewPager2 in my app that gets populated with fragments dynamically based on a response from a websocket request. I also have a AppCompatImageView that has an image set in the XML layout(but later gets a new bitmap to display dynamically).
Now I have a problem that the ViewPager2 and the AppCompatImageView does not show when the app starts, the only way to show them is to force a focus change like opening a popupmenu or alertdialog.
The really weird thing is that I have another imageview in the layout that is set to a static color that is always shown...
Can someone give me a suggestion why those two views gets hidden (like if they were set to View.INVISIBLE, even though they aren't) on app launch, and even better, why do they get displayed after a focus change?
Could the fact that they get populated dynamically interfere in some way with them being rendered correctly?
I realized that my problems was because I had two race conditions.
The first was due to this code:
class ViewPagerFragmentAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
private var arrayList: MutableList<Fragment> = mutableListOf()
fun addFragment(fragment: Fragment) {
arrayList.add(fragment)
}
fun removeFragment(index: Int) {
arrayList.removeAt(index)
}
override fun getItemCount(): Int {
return arrayList.size
}
override fun createFragment(position: Int): Fragment {
return arrayList[position]
}
}
This is the adapter for the ViewPager2 and to solve it I hade to change the addFragment function to:
fun addFragment(fragment: Fragment) {
arrayList.add(fragment)
this.notifyItemInserted(this.itemCount - 1)
}
This due to the fact that if my view rendered after my websocket request finished then I got content but if it rendered before I got the reply I would only have an empty viewpager. By adding the notify call the viewpager gets notified when a fragment is added.(probably need something similar on the remove function as well)
To solve the AppCompatImageView I only changed app:srcCompat="..." in the XML to android:src="...", don't ask me why but then the view rendered...
I'm trying to set up conditional navigation for my Fragments using the Navigation components and a BottomNavigationView.
Current setup (without conditions):
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
NavigationUI.setupWithNavController(bottom_navigation, navController)
General navigation is working fine, but I want to restrict user interaction with the bottom navigation based on conditions. If the condition is not met at the time of clicknig the menu item, this should only result in showing a Toast instead of navigating to the next fragment.
I already looked up this but the solution mentioned there involves navigating to the next fragment first and then check the conditions - but I want to avoid this.
Thank you very much.
Expose LiveData that will stream which #Ids are disabled.
class MainViewModel{
val disableNavigation = MutableLiveData<#Ids Int>()
fun invalidateNavigation() {
val canNavigate = ....
if(!canNavigate){
disableNavigation.value = R.id.bottom_nav_item_x
}
}
}
Then just observe this:
mainViewModel.observe(viewLifecycleOwner){
//disable the id here
//re-enable the rest of the items
}
I added a nullable argument to my start destination:
<?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"
android:id="#+id/nav_graph"
app:startDestination="#id/startDest">
<fragment android:id="#+id/startDest"
android:name="com.myapp.MyStartFragment"
android:label="Start"
tools:layout="#layout/fragment_start">
<argument
android:name="dataObject"
app:argType="com.myapp.MyDataObject"
android:defaultValue="#null"
app:nullable="true"/>
...
</fragment>
...
</navigation>
But when I load my app, I get the following exception:
java.lang.IllegalStateException: Fragment MyStartFragment{a4ffd1f (ca52d4dc-ff36-4a93-8ebf-f11af7b7d5aa) id=0x7f080145} has null arguments
at com.myapp.MyStartFragment$$special$$inlined$navArgs$1.invoke(FragmentNavArgsLazy.kt:42)
at com.myapp.MyStartFragment$$special$$inlined$navArgs$1.invoke(Unknown Source:0)
at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:44)
at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:34)
at com.myapp.MyStartFragment.getArgs(Unknown Source:27)
at com.myapp.MyStartFragment.onAttach(MyStartFragment.kt:85)
And the exception is triggered by this piece of code in MyStartFragment:
private val args: MyStartFragmentArgs by navArgs()
override fun onAttach(context: Context) {
super.onAttach(context)
val title = if(this.args.dataObject == null) getString(R.string.start_list_title) else this.args.dataObject!!.name
...
}
And here is the code for MyDataObject:
#Parcelize
data class MyDataObject (
val id: String,
val name: String,
val externalIdentifier: String
val type: MyDataEnumType,
var responsibleUser: SomeOtherParcelableClass?
): Parcelable
What I don't understand is that my start destination doesn't get passed arguments properly by the navigation controller. Am I missing something here?
Hello I am assuming you want to achieve something like below
BeforeFragment --arg--> StartFragment --> AfterFragment
This flows are similar first time user, returning user flows. Here BeforeFragment is last fragment in login_nav_graph nested graph. StartFragment is the starting destination of main_nav_graph. StartFragment is the first screen returning user sees.
So in BeforeFragment you may set args as follows
val userJohn:User = User(34, "John", 645, UserType.TYPE2, Guardian("Mike"))
val action = BeforeFragmentDirections.actionGlobalStart(userJohn)
findNavController().navigate(action)
and in StartFragment you may do following as you already did
title = if(this.args.user == null)
getString(R.string.user_name) // mocks loading saved user name
else
this.args.user?.name // when user is first time user read from passed args
Sample Repo can be found here
My best guess
This issue is due to bug in older navigation version, so use 2.2.0-alpha01 which is I am using in the sample repo.
In order to fix errors that occurs when moved to new navigation version
in you module gradle file add following
android {
...
kotlinOptions {
jvmTarget = "1.8" // set your Java version here
}
}
This fixes the error
Cannot inline byte code ...
Keep in mind passing complex objects as arguments is not recommended. Quoting from docs.
In general, you should strongly prefer passing only the minimal amount
of data between destinations. For example, you should pass a key to
retrieve an object rather than passing the object itself, as the total
space for all saved states is limited on Android. If you need to pass
large amounts of data, consider using a ViewModel as described in
Share data between fragments.
If this fixes your issue please confirm the answer, since I spent lot of time preparing this post.
If you create a fragment with a bundle, but without NavController.navigate, and you still want to keep "by navArgs()" in destination fragment e.g because you want to use it in launchFragmentInContainer when testing, then use try/catch. The IllegalStateException is thrown before you start checking nullable type.
private fun updateArguments(){
this.title = try{
val args: DestinationFragmentArgs by navArgs()
if (args.dataObject?.name == ""){
// default value provided so check whether bundle was used
arguments?.getString("name") ?: ""
}else {
args?.venueId
}
}catch (ex: Exception){
// fragment created without using NavController.navigate
arguments?.getString("venueId") ?: ""
}
}
Room is very fast, so retrieving an object from the id as an argument should not be an issue, so try using primitive types for your arguments if possible, otherwise use Parcelable or Serialize for inexpensive objects.
I have a layout with some views, one of them has the id title_whalemare
import kotlinx.android.synthetic.main.controller_settings.*
import kotlinx.android.synthetic.main.view_double_text.*
class MainSettingsController : BaseMvpController<MvpView, MvpPresenter>() {
val title: TextView = title_whalemare
override fun getLayout(): Int {
return R.layout.controller_settings
}
}
I try find it with kotlin extensions, but I can't because I get the following error
None of the following candidates is applicable because of receiver type mismatch
controller_settings.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/title_whalemare"/>
</LinearLayout>
Where is my mistake?
Edit: synthetics are now deprecated and will be removed. Please see this blog post for details.
What the error is trying to tell you is that you can only access views with Extensions from either within an Activity or a Fragment. This is because it does the exact same thing to find the views with the given IDs as what you'd be doing manually, it just calls Activity.findViewById() and Fragment.getView().findViewById(), and then does the type cast to the specific subclass of View. I'm guessing that your Controller is not an Activity or Fragment.
There's another way to use Extensions, if you can pass in the root view of your layout to your controller somehow. Then you could do the following:
val title: TextView = rootView.title_whalemare
Again, this is just replacing the View.findViewById() call and the type cast.