Changing theme at runtime from fragment in android kotlin - android

I have created an activity. The activity's layout contains FragmentContainerView element.
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/main_navigation_graph" />
I am showing a fragment into this container using the following code,
val navHostFragment = activity?.supportFragmentManager?.findFragmentById(R.id.nav_host_container) as NavHostFragment
val navController = navHostFragment.navController
navController.navigate(destinationID)
Here i am showing a fragment named as HomeFragment. The home fragment layout contains a linear layout. 50% of space is given to ViewPager2 and 50% is given to a group of 4 buttons, both these layouts are separated vertically. Currently there are multiple items in my viewpager2 and i am looking to change the Ui of these 4 buttons according to the item in the viewpager2. The meaning of changing Ui is to change/switch the entire theme of the fragment or activity at runtime. I have been checking solutions on stack where answers are given to change the theme using styles in OnCreateView before super is called. However it is quite complicated to change the style as i have only one fragment loaded in ContainerView. Hence my question is, can we apply a style to the layout and just change the style of the entire layout on runtime easily or is there any better solution to this ?

Related

Empty state of NavHostFragment of Android Navigation Component

Background
In app I have couple of NavHostFragments that are displayed in different parts of screen:
defaultNavHost is used for switching between tabs
inside one of fragments of defaultNavHost, I have another navHost for BottomSheets
There is a need for another navHost that could display fragments on top of everything else (above tabs, above both of other navHosts). Imagine a tablet layout, that displays a fragment on top of another fragment, just by covering 50% of it.
Issue
A secondary NavHostFragment, as declated below, works as expected when setting nav graph to it.
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="false" />
I set the graph and start destination programmatically:
val navController = findNavController(R.id.fragmentContainerView)
val graph = navController.navInflater.inflate(R.navigation.navigation_additional)
graph.setStartDestination(R.id.navigation_additional_destination_5)
navController.graph = graph
What would be a correct approach to hide navController or navGraph? I want it to dissapear when I do navController.popBackStack(), but it does not work. If I just set fragmentContainerView to gone then fragment will still remain in that NavHostFragment and dissapear animation will not happen. What could be the correct approach to do this? Should the startDestination point to an empty fragment? Or is there a different architectural approach I should be using?

How to create a ViewPager with 3 Fragments inside a Fragment on the same page?

I want to create an application to display 3 different fragments inside a fragment using ViewPager. The fragments will look like in the picture shown down below:
Click here to see the picture
And later on, when user clicks on one of these fragments, another fragment will be opened to display only that specific fragment.
Is this possible? Any recommendations would be appreciated! Thanks!
It really takes 3 minutes.
You have to give each container/view/widget a 0dp which means "match_constrains", in other words: your size will be determined by the constrains you have (after they have been ran by the algorithm).
In the following Layout there are three widgets (I used the "new" FragmentContainerView but they can be FrameLayout instances as well.
Notice in all three:
width/height is set to 0dp (aka: match_constrains)
Fragment one:
Top to Top of Parent (top of the screen)
Start to Start of Parent (left/start of the screen)
End(right) to Start(left) of Fragment Two.
Bottom to Top of Fragment Three.
Fragment Two:
Top to Top of Parent (top of the screen)
Start(left) to End(right) of Parent
End(right) to End of Parent (right/end of the screen)
Bottom to Top of Fragment Three.
Fragment Three:
Top to bottom of Fragment One (here I chose Fragment One, but could have used "two" or even a guideline set at 50%, this is fine)
Start and End both pointing to the respective parent (since we want FR3 to use all the available width)
Bottom to Bottom of Parent (fragment three goes all the way down).
The result looks like this:
And here's the Layout.
Now go and spend some time learning Constraint Layout. :)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragment_one"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/holo_blue_bright"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="#+id/fragment_two"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#id/fragment_three" />
<androidx.fragment.app.FragmentContainerView
android:id="#id/fragment_two"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/holo_blue_dark"
app:layout_constraintStart_toEndOf="#id/fragment_one"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#id/fragment_three" />
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragment_three"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#android:color/holo_purple"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/fragment_one"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Fragment Button is found by Activity's findViewById instead of Fragment's inflated view.findViewById

I'm seeing this strange behavior and couldn't find anything similar to this.
So I have a parent Activity and inside is a Fragment, which I'm including in parent via include element and then in parent's onCreate, create Fragment and replace it with this include layout (Tell me if this is a right way? I was using FrameLayout but then switched to include and defined an id to it).
Activity
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.sourcey.materiallogindemo.CustomerDetailActivity"
tools:ignore="MergeRootFrame">
<com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.appbar.MaterialToolbar />
</com.google.android.material.appbar.AppBarLayout>
<include
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="#id/app_bar"
app:layout_constraintBottom_toTopOf="#id/layout_bottom_bar"
layout="#layout/fragment_customer_detail" />
<androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.bottomappbar.BottomAppBar>
</com.google.android.material.bottomappbar.BottomAppBar>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.sourcey.materiallogindemo.CustomerDetailFragment"
android:layout_height="match_parent"
android:layout_width="match_parent">
<!-- THIS IS THE CULPRIT -->
<com.google.android.material.button.MaterialButton
android:id="#+id/btn_update_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.google.android.material.button.MaterialButton />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/sku_list"
app:layoutManager="LinearLayoutManager"
tools:context="com.sourcey.materiallogindemo.CustomerDetailFragment"
tools:listitem="#layout/fragment_s_k_u_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment is inflated correctly but when I do this inside onCreateView
rootView.btn_update_position.setOnClickListener {
// ... log something
}
and press the Button, it doesn't do anything? Even though most findings were led to this suggestion that I should inflate the view and then set onClickListener.
I also tried doing these
rootView.findViewById<MaterialButton>(R.id.btn_update_position).setOnClickListener {
// ... log something
}
and
val button = rootView.findViewById<MaterialButton>(R.id.btn_update_position)
button.setOnClickListener {
// ... log something
}
but none of them works.
I also tried above approaches in onViewCreated to see if maybe I was not getting the reference but no errors were thrown and no reaction was coming.
Only thing that works is this
activity?.findViewById<MaterialButton>(R.id.btn_update_position)
?.setOnClickListener {
// ... log something
}
I'm trying to understand why this happens? Could this be the issue of using include the Fragment?
NOTE I'm not a pro in android just do hobby work in it so don't know very deeply about it.
EDIT As you can see I have a RecyclerView in Fragment layout, I'm inflating the layout and then setting its adapter items which seems to work fine opposed to button.
rootView.sku_list.adapter = Adapter()
I'm bit confused about what you want to do here
First,<include> doesn't create new view, it just include the xml into the parent xml file so basically it still on activity and you need activity to findViewById
Second, about your question what different between FrameLayout and <include>.
With <include> like i said above, it just add xml file to the parent file, the main usage is for re-use layout (you can include it anywhere) .
With FrameLayout, from official doc : "FrameLayout is designed to block out an area on the screen to display a single item". E.g : you want your layout have a header and footer for all screen, only the middle part change so place a frame layout at middle then load different view for each screen, because that flexibility frame layout usually use for display fragment (you can google how to use frame layout for more details)

BottomNavigationView layout broken, when add menu items programmatically

For some reason BottomNavigationView has a visual bug in layout. Does anyone know any way to fix it? The problem resolves after any button is clicked or after I minimize app and restore it.
This is how it is supposed to look:
Everything works when menu is inflated via XML.
<com.google.android.material.bottomnavigation.BottomNavigationView
android:layout_width="match_parent"
android:layout_height="56dp"
...
app:menu="#menu/bottom_navigation_4_game" />
When I added MenuItem programmatically:
navigationView.menu.clear()
navigationView.inflateMenu(R.menu.bottom_navigation4)
We may see in LayoutInspector, that there are actually 5 items, but two of them are overlayed and not seen:
The problem is probably in BottomNavigationMenuView. In LayoutInspector getWidth() returns 0. Invalidating views didn't help.
If you are trying to create dynamic BottomNavigationView with 2 different menu items set,
So instead of dynamically adding the menu item, use 2 different xml layouts (which have define 2 different app:menu property) and based on the conditions switch between them in your code then.
So, XML would look like this:
<BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="#dimen/bottom_bar_height"
app:elevation="8dp"
app:itemIconTint="?attr/nav_item_color_state_red"
app:itemTextColor="?attr/nav_item_color_state_red"
app:labelVisibilityMode="labeled"
app:menu="#menu/bottom_navigation_1" />
<BottomNavigationView
android:id="#+id/bottom_navigation_mini_player"
android:layout_width="match_parent"
android:layout_height="#dimen/bottom_bar_height"
app:elevation="8dp"
app:itemIconTint="?attr/nav_item_color_state_red"
app:itemTextColor="?attr/nav_item_color_state_red"
app:labelVisibilityMode="labeled"
app:menu="#menu/bottom_navigation_2" />
I found a weird code block in the app, that was to blame. Turns out, that TransitionManager didn't end its transitions with ConstraintLayout. This code: updateConstraints {} was called immediately after dynamically changing BottomNavigationView, hence its child views transition was interrupted, I guess.
private fun updateConstraints(f: ConstraintSet.() -> Unit) {
TransitionManager.beginDelayedTransition(root)
val set = ConstraintSet().apply { clone(root) }
set.f()
set.applyTo(root)
}

Android JetPack navigation with multiple stack

I'm using Jetpack Navigation version 1.0.0-alpha04 with bottom navigation. It works but the navigation doesn't happen correctly. For example, if I have tab A and tab B and from tab A I go to Page C and from there I go to tab B and come back to tab A again, I will see root fragment in the tab A and not page C which does not what I expect.
I'm looking for a solution to have a different stack for each tab, so the state of each tab is reserved when I come back to it, Also I don't like to keep all this fragment in the memory since it has a bad effect on performance, Before jetpack navigation, I used this library https://github.com/ncapdevi/FragNav, That does exactly what, Now I'm looking for the same thing with jetpack navigation.
EDIT 2: Though still no first class support (as of writing this), Google has now updated their samples with an example of how they think this should be solved for now: https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
The major reason is you only use one NavHostFragment to hold the whole back stack of the app.
The solution is that each tab should hold its own back stack.
In your main layout, wrap each tab fragment with a FrameLayout.
Each tab fragment is a NavHostFragment and contains its own navigation graph in order to make each tab fragment having its own back stack.
Add a BottomNavigationView.OnNavigationItemSelectedListener to BottomNavigtionView to handle the visibility of each FrameLayout.
This also takes care of your "...I don't like to keep all this fragment in memory...", because a Navigation with NavHostFragment by default uses fragmentTransaction.replace(), i.e. you will always only have as many fragments as you have NavHostFragments. The rest is just in the back stack of your navigation graph.
Edit: Google is working on a native implementation https://issuetracker.google.com/issues/80029773#comment25
More in detail
Let's say you have a BottomNavigationView with 2 menu choices, Dogs and Cats.
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="#+id/dogMenu"
.../>
<item android:id="#+id/catMenu"
.../>
</menu>
Then you need 2 navigation graphs, say dog_navigation_graph.xml and cat_navigation_graph.xml.
The dog_navigation_graph might look like
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/dog_navigation_graph"
app:startDestination="#id/dogMenu">
</navigation>
and the corresponding for cat_navigation_graph.
In your activity_main.xml, add 2 NavHostFragments
<FrameLayout
android:id="#+id/frame_dog"
...>
<fragment
android:id="#+id/dog_navigation_host_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/dog_navigation_graph"
app:defaultNavHost="true"/>
</FrameLayout>
and underneath add the corresponding for your cat NavHostFragment. On your cat frame layout, set android:visibility="invisible"
Now, in your MainActivity's onCreateView you can
bottom_navigation_view.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.dogMenu -> showHostView(host = 0)
R.id.catMenu -> showHostView(host = 1)
}
return#setOnNavigationItemSelectedListener true
}
All that showHostView() is doing is toggling the visibility of your FrameLayouts that are wrapping the NavHostFragments. So make sure to save them in some way, e.g. in onCreateView
val hostViews = arrayListOf<FrameLayout>() // Member variable of MainActivity
hostViews.apply {
add(findViewById(R.id.frame_dog))
add(findViewById(R.id.frame_cat))
}
Now it's easy to toggle which hostViews should be visible and invisible.
The issue has been resolved by the Android team in the latest version 2.4.0-alpha01 multiple backstacks along with bottom navigation support is now possible without any workaround.
https://developer.android.com/jetpack/androidx/releases/navigation
First, I want to make an edit to #Algar's answer. The frame that you want to hide should have android:visibility="gone" instead of invisible. The reason for that in your main layout you would have something like this:
<LinearLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.activity.MainActivity">
<include
android:id="#+id/toolbar"
layout="#layout/toolbar_base" />
<FrameLayout
android:id="#+id/frame_home"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
>
<fragment
android:id="#+id/home_navigation_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/home_nav" />
</FrameLayout>
<FrameLayout
android:id="#+id/frame_find"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:visibility="gone">
<fragment
android:id="#+id/find_navigation_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/find_nav" />
</FrameLayout>
...
</LinearLayout>
If you wrap your main in a LinearLayout, setting the frame to invisible still make that frame counts, so the BottomNavigation wont appear.
Second, you should create a NavHostFragment instance (ie: curNavHostFragment) to keep track of which NavHostFragment is being visible when a tab in BottomNavigation is clicked. Note: you may want to restore this curNavHostFragment when the activity is destroyed by configuration's changes. This is an example:
#Override
protected void onRestoreInstanceState(#NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
//if this activity is restored from previous state,
//we will have the ItemId of botnav the has been selected
//so that we can set up nav controller accordingly
switch (bottomNav.getSelectedItemId()) {
case R.id.home_fragment:
curNavHostFragment = homeNavHostFragment;
...
break;
case R.id.find_products_fragment:
curNavHostFragment = findNavHostFragment;
...
break;
}
curNavController = curNavHostFragment.getNavController();

Categories

Resources