My activity has a FrameLayout container for fragments and a BottomNavigationView to navigate between the fragments: Home, shop, account and cart. Navigating between these fragments works fine, but when navigating to new fragment (SignUpFragment) from AccountFragment when pushing the button 'signup' it crashes.
using supportFragmentManager didnt work like in MAinActivity so i am trying to use childFragmentManager instead, but the app crashes because it can not find the FrameLayout container in MainActivity.
MainAct:
class MainActivity : AppCompatActivity() {
lateinit var homeFragment: HomeFragment;
lateinit var shopFragment: ShopFragment;
lateinit var accountFragment: AccountFragment;
lateinit var cartFragment: CartFragment;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
homeFragment = HomeFragment()
makeCurrentFragment(homeFragment)
btm_nav.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.home -> {
homeFragment = HomeFragment()
makeCurrentFragment(homeFragment)
}
R.id.shop -> {
shopFragment = ShopFragment()
makeCurrentFragment(shopFragment)
}
R.id.account -> {
accountFragment = AccountFragment()
makeCurrentFragment(accountFragment)
}
R.id.cart -> {
cartFragment = CartFragment()
makeCurrentFragment(cartFragment)
}
}
true
}
}
fun makeCurrentFragment(fragment: Fragment) =
supportFragmentManager.beginTransaction().replace(R.id.frame_layout, fragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) //open = adds a new fragment to the stack
.commit()
}
Layout main:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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:background="#color/colorwhite"
tools:context=".MainActivity"
tools:ignore="ExtraText">
<FrameLayout
android:id="#+id/frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#+id/btm_nav"
android:layout_marginBottom="12dp" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/btm_nav"
android:layout_alignParentBottom="true"
app:itemBackground="#color/colorwhite"
app:menu="#menu/bottom_nav"/>
</RelativeLayout>
AccountFragmenet:
class AccountFragment : Fragment() {
lateinit var signUpFragment: SignUpFragment;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_forgotPass.setOnClickListener {
Toast.makeText(
activity, "btn forgot pressed",
Toast.LENGTH_SHORT
).show()
}
btn_signIn_google.setOnClickListener {
Toast.makeText(
activity, "btn google pressed.",
Toast.LENGTH_SHORT
).show()
}
btn_login.setOnClickListener {
Toast.makeText(
activity, "btn login pressed.",
Toast.LENGTH_SHORT
).show()
}
/********** The problem is here ****/
btn_signup.setOnClickListener {
signUpFragment = SignUpFragment();
childFragmentManager.beginTransaction().replace(R.id.frame_layout, signUpFragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
}
}
}
Errormessage in logcat is :
java.lang.IllegalArgumentException: No view found for id 0x7f0800b7 (no.store.maast:id/frame_layout) for fragment SignUpFragment{b0a086e (7c1e4b61-3b07-48dd-b700-83748b0714c6) id=0x7f0800b7}
you can't access your frame layout in account fragment .
you must replace your fragment with your account fragment root layout .
btn_signup.setOnClickListener {
signUpFragment = SignUpFragment();
childFragmentManager.beginTransaction()
// give an id to your layout root account fragment
.replace(R.id.accountFragmentId, signUpFragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
}
Using ChildFragmentMAnager did not work on mine application no matter what I did, but i found a useful link that solved my problem:
How do i open fragment from fragment (kotlin)
My working code :
btn_signup.setOnClickListener {
signUpFragment = SignUpFragment();
val transaction = activity?.supportFragmentManager?.beginTransaction()
transaction?.replace(R.id.frame_layout, signUpFragment)
transaction?.addToBackStack("BackToSignInPage")
transaction?.commit()
}
Related
I've created a simple, two fragment example app using jetpack Navigation component (androidx.navigation). First fragment navigates to second one, which overrides backbutton behavior with OnBackPressedDispatcher.
activity layout
<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:padding="#dimen/box_inset_layout_padding"
tools:context=".navigationcontroller.NavigationControllerActivity">
<fragment
android:name="androidx.navigation.fragment.NavHostFragment"
android:id="#+id/nav_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</LinearLayout>
FragmentA:
class FragmentA : Fragment() {
lateinit var buttonNavigation: Button
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_a, container, false)
buttonNavigation = view.findViewById<Button>(R.id.button_navigation)
buttonNavigation.setOnClickListener { Navigation.findNavController(requireActivity(), R.id.nav_host).navigate(R.id.fragmentB) }
return view
}
}
<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context=".navigationcontroller.FragmentA">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="fragment A" />
<Button
android:id="#+id/button_navigation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="go to B" />
</LinearLayout>
FragmentB:
class FragmentB : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_b, container, false)
requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val textView = view.findViewById<TextView>(R.id.textView)
textView.setText("backbutton pressed, press again to go back")
this.isEnabled = false
}
})
return view
}
}
<FrameLayout 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"
tools:context=".navigationcontroller.FragmentA">
<TextView
android:id="#+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="fragment B" />
</FrameLayout>
Intended behavior of backbutton in FragmentB (first touch changes text without navigation, second navigates back) works fine when I test the app manually.
I've added instrumented tests to check backbutton behavior in FragmentB and that's where problems started to arise:
class NavigationControllerActivityTest {
lateinit var fragmentScenario: FragmentScenario<FragmentB>
lateinit var navController: TestNavHostController
#Before
fun setUp() {
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
fragmentScenario = FragmentScenario.launchInContainer(FragmentB::class.java)
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
Navigation.setViewNavController(fragment.requireView(), navController)
navController.setLifecycleOwner(fragment.viewLifecycleOwner)
navController.setOnBackPressedDispatcher(fragment.requireActivity().getOnBackPressedDispatcher())
navController.setGraph(R.navigation.nav_graph)
// simulate backstack from previous navigation
navController.navigate(R.id.fragmentA)
navController.navigate(R.id.fragmentB)
}
})
}
#Test
fun whenButtonClickedOnce_TextChangedNoNavigation() {
Espresso.pressBack()
onView(withId(R.id.textView)).check(matches(withText("backbutton pressed, press again to go back")))
assertEquals(R.id.fragmentB, navController.currentDestination?.id)
}
#Test
fun whenButtonClickedTwice_NavigationHappens() {
Espresso.pressBack()
Espresso.pressBack()
assertEquals(R.id.fragmentA, navController.currentDestination?.id)
}
}
Unfortunately, while whenButtonClickedTwice_NavigationHappens passes, whenButtonClickedOnce_TextChangedNoNavigation fails due to text not being changed, just like OnBackPressedCallback was never called. Since app works fine during manual tests, there must be something wrong with test code. Can anyone help me ?
If you're trying to test your OnBackPressedCallback logic, it is better to do that directly, rather than try to test the interaction between Navigation and the default activity's OnBackPressedDispatcher.
That would mean that you'd want to break the hard dependency between the activity's OnBackPressedDispatcher (requireActivity().onBackPressedDispatcher) and your Fragment by instead injecting in the OnBackPressedDispatcher, thus allowing you to provide a test specific instance:
class FragmentB(val onBackPressedDispatcher: OnBackPressedDispatcher) : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_b, container, false)
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val textView = view.findViewById<TextView>(R.id.textView)
textView.setText("backbutton pressed, press again to go back")
this.isEnabled = false
}
})
return view
}
}
This allows you to have your production code provide a FragmentFactory:
class MyFragmentFactory(val activity: FragmentActivity) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
when (loadFragmentClass(classLoader, className)) {
FragmentB::class.java -> FragmentB(activity.onBackPressedDispatcher)
else -> super.instantiate(classLoader, className)
}
}
// Your activity would use this via:
override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = MyFragmentFactory(this)
super.onCreate(savedInstanceState)
// ...
}
This would mean you could write your tests such as:
class NavigationControllerActivityTest {
lateinit var fragmentScenario: FragmentScenario<FragmentB>
lateinit var onBackPressedDispatcher: OnBackPressedDispatcher
lateinit var navController: TestNavHostController
#Before
fun setUp() {
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
// Create a test specific OnBackPressedDispatcher,
// giving you complete control over its behavior
onBackPressedDispatcher = OnBackPressedDispatcher()
// Here we use the launchInContainer method that
// generates a FragmentFactory from a constructor,
// automatically figuring out what class you want
fragmentScenario = launchFragmentInContainer {
FragmentB(onBackPressedDispatcher)
}
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
Navigation.setViewNavController(fragment.requireView(), navController)
navController.setGraph(R.navigation.nav_graph)
// Set the current destination to fragmentB
navController.setCurrentDestination(R.id.fragmentB)
}
})
}
#Test
fun whenButtonClickedOnce_FragmentInterceptsBack() {
// Assert that your FragmentB has already an enabled OnBackPressedCallback
assertTrue(onBackPressedDispatcher.hasEnabledCallbacks())
// Now trigger the OnBackPressedDispatcher
onBackPressedDispatcher.onBackPressed()
onView(withId(R.id.textView)).check(matches(withText("backbutton pressed, press again to go back")))
// Check that FragmentB has disabled its Callback
// ensuring that the onBackPressed() will do the default behavior
assertFalse(onBackPressedDispatcher.hasEnabledCallbacks())
}
}
This avoids testing Navigation's code and focuses on testing your code and specifically your interaction with OnBackPressedDispatcher.
The reason for FragmentB's OnBackPressedCallback to be ignored is the way how OnBackPressedDispatcher treats its OnBackPressedCallbacks. They are run as chain-of-command, meaning that most recently registered one that is enabled will 'eat' the event so others will not receive it. Therefore, most recently registered callback inside FragmentScenario.onFragment() (which is enabled by lifecycleOwner, so whenever Fragment is at least in lifecycle STARTED state. Since fragment is visible during the test when backbutton is pressed, callback is always enabled at the time), will have priority over previously registered one in FragmentB.onCreateView().
Therefore, TestNavHostController's callback must be added before FragmentB.onCreateView() is executed.
This leads to changes in test code #Before method:
#Before
fun setUp() {
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
fragmentScenario = FragmentScenario.launchInContainer(FragmentB::class.java, initialState = Lifecycle.State.CREATED)
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
navController.setLifecycleOwner(fragment.requireActivity())
navController.setOnBackPressedDispatcher(fragment.requireActivity().getOnBackPressedDispatcher())
navController.setGraph(R.navigation.nav_graph)
// simulate backstack from previous navigation
navController.navigate(R.id.fragmentA)
navController.navigate(R.id.fragmentB)
}
})
fragmentScenario.moveToState(Lifecycle.State.RESUMED)
fragmentScenario.onFragment(object : FragmentScenario.FragmentAction<FragmentB> {
override fun perform(fragment: FragmentB) {
Navigation.setViewNavController(fragment.requireView(), navController)
}
})
}
Most important change is to launch Fragment in CREATED state (instead of default RESUMED) to be able to tinker with it before onCreateView().
Also, notice that Navigation.setViewNavController() is run in separate onFragment() after moving fragment to RESUMED state - it accepts View parameter, so it cannot be used before onCreateView()
For example I have one activity and three fragments (1, 2, 3).
I show first fragment, then second fragment (adding it to back stack) and third fragment (no adding it to back stack because when pressing the back button I want to go to the first fragment from the current third one instead of going to the second fragment)
This logic works but when it go back to first fragment, third fragment is still visible (two fragments are displayed at the same time)
Here's the full flow example and the full code when Fragment 1 and Fragment 3 are displayed at the same time
Activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
showFragment(
addToBackStack = false,
backStackName = null,
fragmentNumber = 1,
tag = "FRAGMENT_FIRST"
)
// simulate going to other fragments sequentially
val mainHandler = Handler(Looper.getMainLooper())
mainHandler.postDelayed(
{
showFragment(
addToBackStack = true,
backStackName = null,
fragmentNumber = 2,
tag = "FRAGMENT_SECOND"
)
},
1000L
)
mainHandler.postDelayed(
{
showFragment(
addToBackStack = false, // when back pressing from third fragment I want it to get back to first fragment, not second
backStackName = null,
fragmentNumber = 3,
tag = "FRAGMENT_THIRD"
)
},
2000L
)
}
private fun showFragment(
addToBackStack: Boolean,
backStackName: String?,
fragmentNumber: Int,
tag: String
) {
val transaction = supportFragmentManager.beginTransaction()
var fragment =
supportFragmentManager.findFragmentByTag(tag) as FragmentGeneral?
if (fragment == null) {
fragment = FragmentGeneral.newInstance(number = fragmentNumber)
transaction.replace(R.id.fragment_container, fragment, tag)
if (addToBackStack) {
transaction.addToBackStack(backStackName)
}
} else {
transaction.show(fragment)
}
transaction.commit()
}
}
class FragmentGeneral : Fragment() {
private lateinit var binding: FragmentGeneralBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
) = FragmentGeneralBinding.inflate(layoutInflater, container, false).apply {
binding = this
}.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
arguments?.getInt(ARG_FRAGMENT_NUMBER)?.let { number ->
binding.fragmentNumber = number
// set different position of the text so we could clearly see overlapping of two fragments
binding.textView.gravity = when (number) {
1 -> Gravity.TOP
2 -> Gravity.CENTER_VERTICAL
3 -> Gravity.BOTTOM
else -> return
}
// simulate pressing back button pressing
if (number == 3) {
// should go to first fragment because it wasn't added to backstack
Handler(Looper.getMainLooper()).postDelayed(
{ activity?.onBackPressed() },
1000L
)
}
}
}
companion object {
private const val ARG_FRAGMENT_NUMBER = "ARG_FRAGMENT_NUMBER"
fun newInstance(number: Int) =
FragmentGeneral().apply {
arguments = bundleOf(ARG_FRAGMENT_NUMBER to number)
}
}
}
activity_main.xml:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</layout>
fragment_general.xml:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".FragmentGeneral">
<data>
<variable
name="fragmentNumber"
type="int" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="#+id/text_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:text='#{"Fragment #" + fragmentNumber}'
android:textColor="#android:color/black"
android:textSize="64sp"
tools:text="Fragment #?" />
</FrameLayout>
</layout>
So why third fragment is still visible? How to fix this issue and what is correct way to handle such transactions?
Developers usually would not notice such issues if their fragments are not transparent, but in my example it's transparent, so the white background - it's activity background
I'm trying to get a viewpager in an already existing fragment.
In another project I had no issues when the viewpager was in activity_main.xml
One of the things I tried is the following:
ViewPagerAdapter.kt
class ViewPagerAdapter(manager: FragmentManager) : FragmentPagerAdapter(manager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItem(position: Int): Fragment {
return when (position) {
0 -> KapperKeuze1Fragment()
1 -> KapperKeuze2Fragment()
else -> KapperKeuze3Fragment()
}
}
override fun getCount(): Int {
return 3
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var homeFragment: HomeFragment
lateinit var afspraakFragment: AfspraakFragment
lateinit var overFragment: OverFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val bottomNavigation : BottomNavigationView = findViewById(R.id.bot_nav)
homeFragment = HomeFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.frame_layout, homeFragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
bottomNavigation.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.home -> {
homeFragment = HomeFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.frame_layout, homeFragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
}
R.id.afspraak -> {
afspraakFragment = AfspraakFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.frame_layout, afspraakFragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
}
R.id.over -> {
overFragment = OverFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.frame_layout, overFragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
.commit()
}
}
true
}
val adapter = ViewPagerAdapter(supportFragmentManager)
val pager = findViewById<View>(R.id.viewPager) as ViewPager
pager.adapter = adapter
}
}
fragment_afspraak.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager.widget.ViewPager
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
The error occurs in MainActivity.kt at: val pager = findViewById<View>(R.id.viewPager) as ViewPager
And gives:
Caused by: kotlin.TypeCastException: null cannot be cast to non-null type androidx.viewpager.widget.ViewPager
Use childFragmentManager instead of supportFragmentManager
Because it returns a private FragmentManager for placing and managing Fragments inside of this Fragment.(You ViewPager fragments located in you afspraak fragment)
So set your ViewPager in your AfspraakFragment as below :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = ViewPagerAdapter(childFragmentManager)
val pager = view.findViewById(R.id.viewPager) as ViewPager
pager.adapter = adapter
}
As the title suggests, I'm trying to change a fragment's view/button's visibility from an activity.
Fragment's code:
package nus.is3261.kotlinapp
import android.content.Context
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
*
*/
class SettingFragment : Fragment() {
private var listener:SettingFragment.OnFragmentInteractionListener? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_setting, container, false)
val signIn = view.findViewById<View>(R.id.btn_sign_in)
signIn.setOnClickListener {
onButtonPressed("signIn")
}
val signOut = view.findViewById<Button>(R.id.btn_sign_out)
signOut.setOnClickListener {
onButtonPressed("signOut")
}
return view
}
fun changeVisibility(isSignedIn : Boolean){
if (isSignedIn) {
val signIn = view?.findViewById<View>(R.id.btn_sign_in)
signIn?.visibility = View.GONE
val signOut = view?.findViewById<View>(R.id.btn_sign_out)
signOut?.visibility = View.VISIBLE
} else {
val signIn = view?.findViewById<View>(R.id.btn_sign_in)
signIn?.visibility = View.VISIBLE
val signOut = view?.findViewById<View>(R.id.btn_sign_out)
signOut?.visibility = View.GONE
}
}
fun onButtonPressed(str: String) {
listener?.onFragmentInteraction(str)
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is SettingFragment.OnFragmentInteractionListener) {
listener = context
} else {
throw RuntimeException(context.toString() + " must implement OnFragmentInteractionListener")
}
}
override fun onDetach() {
super.onDetach()
listener = null
}
interface OnFragmentInteractionListener {
fun onFragmentInteraction(str: String)
}
}
As you can already see, I have the changeVisibility function to change the visibility of the buttons already set up. Now, how can I invoke these functions from the main activity ?
I tried this from the main activity but it does not work sadly:
private fun updateUI(user: FirebaseUser?) {
if (user != null) {
// tvStatus.text = "Google User email: " + user.email!!
// tvDetail.text = "Firebase User ID: " + user.uid
val fragment = SettingFragment()
fragment.changeVisibility(true)
// btn_sign_in.visibility = View.GONE
// layout_sign_out_and_disconnect.visibility = View.VISIBLE
} else {
// tvStatus.text = "Signed Out"
// tvDetail.text = null
val fragment = SettingFragment()
fragment.changeVisibility(false)
// btn_sign_in.visibility = View.VISIBLE
// layout_sign_out_and_disconnect.visibility = View.GONE
}
}
Here is my xml file:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:background="#color/dracula"
tools:context=".SettingFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.gms.common.SignInButton
android:id="#+id/btn_sign_in"
android:layout_width="match_parent"
android:layout_weight="1"
android:layout_height="0dp"
android:visibility="visible"
tools:visibility="gone" />
<Button
android:id="#+id/btn_sign_out"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone"
tools:visibility="visible"
android:backgroundTint="#color/draculalight"
android:textColor="#color/green"
android:text="#string/signout" />
</LinearLayout>
</FrameLayout>
Ok so you have a couple of issues, but it's probably best if I just provide a thorough step by step for you. So let's start from the beginning.
So first the Issue
You are referencing the wrong memory. First you put a fragment into your xml, then you new up a different instance of it, so it's like pouring a cup of coffee, then drinking out of a new empty cup and wondering why the coffee isn't in there.
Now for the solution.
First your MainActivity (or parent activity of the fragment) MUST contain the element of the fragment you are trying to include. You have a couple of options to do this. Let's start with the simplest way assuming it is a static fragment that will not be swapped out.
OPTION 1 (Fixed Fragment)
<ParentActivityLayoutOfYourChoice>
<fragment
android:name="com.yourpath.FooFragment"
android:id="#+id/fooFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</ParentActivityLayoutOfYourChoice>
Then in the Activity you would simply create a member variable and access it like:
//lateinit only if you guarantee it will be there in the oncreate
private lateinit var fooFragment: FooFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fooFragment = findViewById(R.id.fooFragment)
}
fun btnSignIn_onClick(){
//onSuccess
fooFragment.isSignedIn(true)
}
OPTION 2 (Dynamic Fragments)
<ParentActivityLayoutOfYourChoice>
<FrameLayout
android:id="#+id/fragPlaceholder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</ParentActivityLayoutOfYourChoice>
Then of course you can create the Fragment in your onCreate or appropriate place (such as drawer switching fragments) and swap it into the place holder.
EXAMPLE:
//Inside MainActivity (or parent activity)
//lazy will new it up the first time you use it.
private val mFooFragment by lazy {
FooFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
swapFragment(mFooFragment) //will auto new the fragment with lazy
}
//Let's start simple before I show you thorough
fun swapFragment(fragment: Fragment){
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragPlaceholder, fragment)
fragmentTransaction.commit() //puts the fragment into the placeholder
}
fun btnSignIn_onClick(){
//onSuccess
mFooFragment.isSignedIn(true)
}
*Now, before we go further, I feel it's important that I tell you that if you are swapping fragments dynamically there is MUCH more to it. You should handle the bundle, you should know if you are hiding, showing, replacing, etc.. There are many ways to handle the transaction. When changing your fragment out, you have to decide if you are hiding or removing. It will affect the lifecycle for onResume vs onCreate when you put them back, so manage it wisely.
I have built a simple swapFragment method that I use in almost all my projects in a BaseActivity. I'll share that now just to be thorough.
EXAMPLE OF STORING SELECTED FRAGMENT AND CONTROLLING THE SWAPPING OF FRAGMENTS DYNAMICALLY IN A BASE ACTIVITY
private var mSelectedFragment: BaseFragment? = null
protected fun swapFragment(fragment: BaseFragment, #Nullable bundle: Bundle?, hideCurrentFrag: Boolean = false) {
if (fragment.isVisible) {
A35Log.e(mClassTag, "swapFragment called on already visible fragment")
return
}
A35Log.v(mClassTag, "swapFragment( ${fragment.javaClass.simpleName} )")
val currentFragBundle = fragment.arguments
if (currentFragBundle == null && bundle != null) {
fragment.arguments = bundle
A35Log.v(mClassTag, "current bundle is null, so setting new bundle passed in")
} else if (bundle != null) {
fragment.arguments?.putAll(bundle)
A35Log.v(mClassTag, "current fragment bundle was not null, so add new bundle to it")
}
//make sure no pending transactions are still floating and not complete
val fragmentManager = supportFragmentManager
fragmentManager.executePendingTransactions()
val fragmentTransaction = fragmentManager.beginTransaction()
//Make sure the requested fragment isn't already on the screen before adding it
if (fragment.isAdded) {
A35Log.v(mClassTag, "Fragment is already added")
if (fragment.isHidden) {
A35Log.v(mClassTag, "Fragment is hidden, so show it")
fragmentTransaction.show(fragment)
if(hideCurrentFrag) {
A35Log.v(mClassTag, "hideCurrentFlag = true, hiding current fragment $mSelectedFragment")
fragmentTransaction.hide(mSelectedFragment!!)
}else{
A35Log.v(mClassTag, "hideCurrentFlag = false, removing current fragment $mSelectedFragment")
fragmentTransaction.remove(mSelectedFragment!!)
}
}else{
A35Log.v(mClassTag, "Fragment is already visible")
}
}else if(mSelectedFragment == null){
A35Log.v(mClassTag,"mSelectedFragment = null, so replacing active fragment with new one ${fragment.javaClass.simpleName}")
fragmentTransaction.replace(R.id.fragPlaceholder, fragment)
}else{
A35Log.v(mClassTag, "Fragment is not added, so adding to the screen ${fragment.javaClass.simpleName}")
fragmentTransaction.add(R.id.fragPlaceholder, fragment)
if(hideCurrentFrag) {
A35Log.v(mClassTag, "hideCurrentFlag = true, hiding current fragment $mSelectedFragment")
fragmentTransaction.hide(mSelectedFragment!!)
}else{
A35Log.v(mClassTag, "hideCurrentFlag = false, removing current fragment $mSelectedFragment")
fragmentTransaction.remove(mSelectedFragment!!)
}
}
A35Log.v(mClassTag, "committing swap fragment transaction")
fragmentTransaction.commit()
A35Log.v(mClassTag, "mSelectedFragment = ${fragment.javaClass.simpleName}")
mSelectedFragment = fragment
}
All Examples are provided in Kotlin since that is where Android is headed and you should be learning in Kotlin rather than Java if you are not already. If you are working with Java, then you can paste this into a Java file and I believe it will offer to translate it to Java for you.
Happy Coding!
This solved it for me in the end:
private fun updateUI(user: FirebaseUser?) {
if (user != null) {
// tvStatus.text = "Google User email: " + user.email!!
// tvDetail.text = "Firebase User ID: " + user.uid
var fragment = supportFragmentManager.findFragmentByTag("setting") as SettingFragment
fragment.changeVisibility(true)
// btn_sign_in.visibility = View.GONE
// layout_sign_out_and_disconnect.visibility = View.VISIBLE
} else {
// tvStatus.text = "Signed Out"
// tvDetail.text = null
var fragment = supportFragmentManager.findFragmentByTag("setting") as SettingFragment
fragment.changeVisibility(false)
// btn_sign_in.visibility = View.VISIBLE
// layout_sign_out_and_disconnect.visibility = View.GONE
}
}
I'm starting in Kotling and I don't know how to change between fragments, I have tried this code:
val manager = supportFragmentManager
val transaction = manager.beginTransaction()
transaction.add(R.layout.fragment_information.toInt(), ComplainFragment())
transaction.commit()
R.layout.fragment_information.toInt()
But i have an error with this parameter because it doesn't find the fragment Id.
I usually use replace to change between fragments. Also change R.layout.fragment_information to R.id.fragment_layout_id only, so no need toInt()
transaction.replace(R.id.fragment_layout_id, fragment)
Here is my suggestion.
var fragment: Fragment? = null
when (itemId) {
R.id.fragment_information -> {
fragment = ComplainFragment()
}
}
if (fragment != null) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_layout_id, fragment)
transaction.commit()
}
The other answers will work but still we can improve a lot by using extension functions in Kotlin.
Add an extension function to the FragmentManager class like below,
inline fun FragmentManager.doTransaction(func: FragmentTransaction.() ->
FragmentTransaction) {
beginTransaction().func().commit()
}
then create an extension function to the AppCompatActivity class,
fun AppCompatActivity.addFragment(frameId: Int, fragment: Fragment){
supportFragmentManager.doTransaction { add(frameId, fragment) }
}
fun AppCompatActivity.replaceFragment(frameId: Int, fragment: Fragment) {
supportFragmentManager.doTransaction{replace(frameId, fragment)}
}
fun AppCompatActivity.removeFragment(fragment: Fragment) {
supportFragmentManager.doTransaction{remove(fragment)}
}
Now, to add and remove fragments from any activity, you just need to call like this,
addFragment(R.id.fragment_container, fragment)
replaceFragment(R.id.fragment_container, fragment)
please refer the below link for more info,
https://medium.com/thoughts-overflow/how-to-add-a-fragment-in-kotlin-way-73203c5a450b
This is an example for you to go to a fragment or activity by clicking a button inside another fragment:
class Fragment_One: Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_one, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_goToActivity2.setOnClickListener {
val intent = Intent(context, SecondActivity::class.java)
startActivity(intent)
}
btn_goToFragment2.setOnClickListener {
var fr = getFragmentManager()?.beginTransaction()
fr?.replace(R.id.fragment, Fragment_Two())
fr?.commit()
}
}
}
When you add a fragment, you need to add it to an ID that exists in your Activity's layout, not an entire layout:
supportFragmentManager.beginTransaction().add(R.id.some_id_in_your_activity_layout, ComplainFragment()).commit()
In case anyone still needs a quick approach to this. I created a function than can be easily called whenever you need to change a fragment.
private fun replaceFragment(fragment: Fragment) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.frame, fragment)
transaction.commit()
}
R.id.frame in this case is the id of my Framelayout in the activity that will hold my fragment. All you have to do now is call the function.
replaceFragment(HomeFragment())
private fun transitionFragment(fragment: Fragment) {
val transition = requireActivity().supportFragmentManager.beginTransaction()
transition.replace(R.id.fragment_container_create_void_parent, fragment)
.addToBackStack(null).commit()
}
fragment-ktx jetpack library contains convenient extension functions which simplify many things, including transactions:
// MyActivity.kt
class MyActivity : AppCompatActivity() {
...
fun showMyFragment() {
val fragment = MyFragment()
supportFragmentManager.commit {
replace(R.id.fragment_container, fragment)
}
}
}
R.id.fragment_container it's an id of a fragment container in the parent layout. There's FragmentContainerView which is the recommended container, for example:
<!-- my_activity_layout.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout ...>
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragment_container"
... />
...
</androidx.constraintlayout.widget.ConstraintLayout>
But if your purpose is to implement in-app navigation, it's better and much easier to use Navigation component instead of manually switching fragments.
this is my solution for Change current fragment to orther in kotlin:
val supportFragment = SupportFragment()
requireActivity().supportFragmentManager.beginTransaction()
.add(this.id, supportFragment)
.addToBackStack("ok")
.commit()