I'm attempting to add Jetpack Compose composables to my xml file in a fragment.
When I try to run it on a device I'm getting an error:
Cannot add views to ComposeView; only Compose content is supported
Fragment:
class ComposeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(
R.layout.activity_main, container, false
)
view.findViewById<ComposeView>(R.id.compose_view).setContent {
Column(
modifier = Modifier
.border(border = BorderStroke(1.dp, Color.Black))
.padding(16.dp)
) {
Text("THIS IS A COMPOSABLE INSIDE THE FRAGMENT XML")
Spacer(modifier = Modifier.padding(10.dp))
CircularProgressIndicator()
Spacer(modifier = Modifier.padding(10.dp))
Text("NEAT")
Spacer(modifier = Modifier.padding(10.dp))
}
}
return view
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/main_container"
/>
<androidx.compose.ui.platform.ComposeView
android:id="#+id/compose_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</RelativeLayout>
MainActivity.kt:
open class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.replace(R.id.compose_view, ComposeFragment())
.commit()
}
}
I'm not really sure what this error means or how to address it. I can't find this error anywhere by googling.
In your MainActivity you are trying to put a fragment inside a ComposeView (which takes only Composable).
You should specify the right container for your Fragment:
supportFragmentManager.beginTransaction()
.replace(R.id.main_container, ComposeFragment())
.commit()
replace(R.id.compose_view, ComposeFragment()) is adding a Fragment to your ComposeView, not to your FragmentContainerView - that's what is adding a View to your ComposeView.
If you want to add a Fragment to your FragmentContainerView, you'll need to use the ID of the FragmentContainerView:
open class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Always surround any FragmentTransactions in a
// savedInstanceState == null since fragment are
// automatically restored and you don't want to replace
// those restored fragments
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.main_container, ComposeFragment())
.commit()
}
}
}
It isn't clear why your Activity is inflating activity_main and your Fragment is also inflating activity_main - those should be different layouts and you should choose if you want your activity's layout to only host a fragment (thereby removing the ComposeView from its layout) and have the fragment contain your ComposeView or if you want to skip the fragment entirely and directly have your ComposeView in your Activity. Doing both (in particular, in your layout where they would overlap one another due to your use of RelativeLayout) doesn't make much sense.
Related
I want to open an existing fragment from compose or if i can add inside the same compose in full screen, that will also work. Also how to navigate from one existing fragment to another existing fragment from compose.
Our recommendation for using Fragments in Compose is documented here.
Specifically, you should use the AndroidViewBinding composable to inflate an XML containing a FragmentContainerView hosting the fragment you want to use in Compose. AndroidViewBinding has fragment-specific handling which is why you want to use this over AndroidView. Note that you need to have view binding enabled for this.
Example XML file:
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragment_container_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:name="com.example.MyFragment" />
And then in your Composable function:
#Composable
fun FragmentInComposeExample() {
AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
val myFragment = fragmentContainerView.getFragment<MyFragment>()
// ...
}
}
I solved this as following:
#Composable
fun ReusableFragmentComponent(
someArgumentForFragment: FragmentArgument,
fragmentManager: FragmentManager,
modifier: Modifier = Modifier,
tag: String = "ReusableFragmentTag"
) {
AndroidView(
modifier = modifier,
factory = { context ->
FrameLayout(context).apply {
id = ViewCompat.generateViewId()
}
},
update = {
val fragmentAlreadyAdded = fragmentManager.findFragmentByTag(tag) != null
if (!fragmentAlreadyAdded) {
fragmentManager.commit {
add(it.id, ReusableFragment.newInstance(someArgumentForFragment), tag)
}
}
}
)
}
In my case I called this from a fragment (hosted by navigation component), in an effort to make our reusable fragments compose compatible. I did that like so:
class ReusableFragments : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
Column {
Text("ReusableFragment 1")
ReusableFragmentComponent(someArgument1, childFragmentManager, tag = "ReusableFragmentTag1")
Text("ReusableFragment 2")
ReusableFragmentComponent(someArgument2, childFragmentManager, tag = "ReusableFragmentTag2")
}
}
}
}
}
By making the tag customizable it's possible to add multiple different instances of the same fragment to the same fragment manager.
In order to work with your existing xml views and fragments, in a Jetpack Compose app, you can refer to Interoperability APIs.
Specifically, I understand that you want to manage Android Views in Compose.
I want to open an existing fragment from compose
#Composable
fun CustomView() {
AndroidView(
factory = { context ->
MyView(context).apply {
// myView listeners
}
},
update = { view ->
// Recomposition logics
}
)
}
..how to navigate from one existing fragment to another existing fragment from compose.
In the above example you now have a composable function, so just use Jetpack Compose navigation library.
This helped me:
#Composable
fun MyFragmentView(
fragmentManager: FragmentManager,
modifier: Modifier = Modifier
) {
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { context ->
val containerId = R.id.container // some unique id
val fragmentContainerView = FragmentContainerView(context).apply {
id = containerId
}
val fragment = MyFragment()
fragmentManager.beginTransaction()
.replace(containerId, fragment, fragment.javaClass.simpleName)
.commitAllowingStateLoss()
fragmentContainerView
}
)
}
ids.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="container" type="id"/>
</resources>
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()
I have a very weird problem. When I navigate from Fragment 1 to Fragment 2 using a btn.setOnClickListener and then navigating back from Fragment 2 to Fragment 1 using the back button, the btn.setOnClickListener in Fragment 1 does not work anymore and therefore is not able to navigate to Fragment 2 again.
Here is my code:
Button XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
// Custom Background for the button
<com.google.android.material.appbar.MaterialToolbar
android:clickable="false"
android:id="#+id/materialToolbar"
android:layout_width="match_parent"
android:layout_height="90dp"
android:background="#color/btnColorGray"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.button.MaterialButton
android:clickable="true"
android:focusable="true" />
</androidx.constraintlayout.widget.ConstraintLayout>
Main XML
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.view.fragments.home.calibrateAndRepair.CalibrateRepairMessageFragment">
... some other stuff
<!-- Included the button -->
<include
android:id="#+id/calibrate_repair_btn"
layout="#layout/calibrate_repair_btn"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
BaseFragment for databinding
abstract class BaseFragment<out T: ViewDataBinding>(val layout: Int) : Fragment() {
abstract val viewModel: ViewModel
private val _navController by lazy { findNavController() }
val navController: NavController
get() = _navController
fun navigateTo(fragment: Int, bundle: Bundle? = null) {
_navController.navigate(fragment, bundle)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return DataBindingUtil.inflate<T>(inflater, layout, container, false).apply {
lifecycleOwner = viewLifecycleOwner
setVariable(BR.viewModel, viewModel)
Timber.d("Created BaseFragment and binded View")
}.root
}
}
EmailFragment for initializing the button
abstract class EmailFragment<out T: ViewDataBinding>(
layout: Int,
private val progressBarDescription: ArrayList<String>,
private val stateNumber: StateProgressBar.StateNumber
) : BaseFragment<T>(layout) {
abstract val next: Int
abstract val bundleNext: Bundle?
// getting the button from the button.xml
private val btnNext: MaterialButton by lazy { btn_next_calibrate }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ... some other initializing which constantly work!
initButton()
}
// Initializing the button
private fun initButton() {
btnNext.setOnClickListener {
navigateTo(next, bundleNext)
Timber.d("Button clicked")
}
}
}
Fragment 1
#AndroidEntryPoint
class CalibrateRepairMessageFragment(
private val progressBarDescription: ArrayList<String>,
#StateNumberOne private val stateNumber: StateProgressBar.StateNumber,
) : EmailFragment<FragmentCalibrateRepairMessageBinding>(
R.layout.fragment_calibrate_repair_message,
progressBarDescription,
stateNumber
) {
// Overriding the values from EmailFragment
override val next: Int by lazy { R.id.action_calibrateRepairMessageFragment_to_calibrateRepairUserDataFragment }
override val bundleNext: Bundle by lazy { bundleOf("calibrate_repair_toolbar_text" to toolbarText) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ... some other stuff
}
}
Fragment 2
#AndroidEntryPoint
class CalibrateRepairUserDataFragment(
private val progressBarDescription: ArrayList<String>,
#StateNumberTwo private val stateNumber: StateProgressBar.StateNumber,
) : EmailFragment<FragmentCalibrateRepairUserDataBinding>(
R.layout.fragment_calibrate_repair_user_data,
progressBarDescription,
stateNumber
) {
override val next: Int by lazy { R.id.action_calibrateRepairUserDataFragment_to_calibrateRepairDataOverviewFragment }
override val bundleNext: Bundle by lazy { bundleOf("calibrate_repair_toolbar_text" to toolbarText) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
I tried to delete everything that is not important for the question. You can ignore the constructor of BaseFragment, EmailFragment, CalibrateRepairMessageFragment and CalibrateRepairUserDataFragment. I am using navigation component and dagger-hilt.
I appreciate every help, thank you.
P.S: I've noticed that using button:onClick in the .xml file solves this problem but in this situation I can't use the xml version.
The problem with this should be your lazy initialization of btnNext.
The Fragment1 state is saved when navigating to Fragment2. When navigating back the XML View will be reloaded but the lazy value of btnNext won't change as it is already initialized and is pointing to the old reference of the button view. Thus your OnClickListener will always be set to the old reference.
Instead of assigning your button lazily you should assign it in EmailFragment's onCreateView()
PS: Also from btn_next_calibrate I suppose you are using kotlin synthetic view binding. If so you would not have to use a class variable.
I'm just getting into Android development in Kotlin, and I'm not quite sure what the correct approach to this situation is.
Essentially what I'm trying to do is have a ViewPager that scrolls through a list of items of the same type. The view associated with each item has a button. When I press the button, I would like to launch a new fragment, let's call it iteminfo. The iteminfo fragment should use the back stack so that you can hit the back button and return to the item view you were looking at before you hit the button.
The way I have it set up right now is this:
-My MainActivity add()s my ViewPager-containing fragment to the FragmentManager. (I am going for a one-activity architecture.)
-My ViewPager fragment contains a private inner class with a PagerAdapter that maintains a list of item fragments. The item fragments are generated and added to the PagerAdapter when the ViewPager fragment is created.
-The item fragments each have their own button. When the fragment is created, the button gets an onClickListener.
This is where I run into a bit of a dead end...
So far I've managed to get the iteminfo fragment to display and the item fragment to disappear, but my method was pretty janky: I performed a fragment transaction to add() the iteminfo fragment to the root element of my item_fragment.xml and then just set the visibility of the other elements in there to GONE. Cosmetically this gets me there, however if I hit the back button, the app just exits. So this is not going to cut it.
I have read that when switching between fragment views it is a good design to use a single empty FrameLayout in the .xml file as a fragment container and then add/remove fragments from that using the FragmentManager. This makes sense to me...
However, I am not sure how to remove() a fragment that is inside of a ViewPager. AFAIK you can't set a fragment's tag unless you're doing a fragment transaction. So if I want to perform replace(), I don't know how to reference the fragment I'm replacing.
Just as a Hail-Mary, I tried just replacing the whole ViewPager fragment (since this is added in MainActivity via a fragment transaction, I was able to tag it), but for some reason that just yields a NPE. It makes sense that a child fragment should not be able to replace its parent fragment, although I'm not 100% sure why it's a NPE.
Anyways, any enlightenment you can ship my way is greatly appreciated.
Edit: Added relevant code below.
MainActivity.kt:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val currentFragment = supportFragmentManager
.findFragmentById(R.id.fragment_container)
if (currentFragment == null) {
val fragment = ItemPagerFragment.newInstance()
supportFragmentManager
.beginTransaction()
.add(R.id.fragment_container, fragment, "viewPager")
.commit()
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
ItemPagerFragment.kt:
class ItemPagerFragment: Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var pagerAdapter: ItemPagerAdapter
private val itemListViewModel: ItemListViewModel by lazy {
ViewModelProviders.of(this).get(ItemListViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_item_pager, container, false)
viewPager = view.findViewById(R.id.item_viewpager) as ViewPager
pagerAdapter = ItemPagerAdapter(getChildFragmentManager())
val items = itemListViewModel.items
for (item in items){
pagerAdapter.addFrag(ItemFragment.newInstance(item))
}
viewPager.adapter = pagerAdapter
return view
}
private inner class ItemPagerAdapter(fm: FragmentManager)
: FragmentStatePagerAdapter(fm)
{
private val itemFragmentList: MutableList<ItemFragment> = ArrayList()
override fun getItem(i: Int): Fragment {
return itemFragmentList[i]
}
override fun getCount() = itemFragmentList.size
fun addFrag(f: ItemFragment){
itemFragmentList.add(f)
}
}
companion object {
fun newInstance(): ItemPagerFragment {
return ItemPagerFragment()
}
}
}
fragment_item_pager.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager.widget.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/item_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
ItemFragment.kt:
class ItemFragment(i: Item): Fragment() {
private var item: Item = i;
private lateinit var infoButton: Button
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_item, container, false)
infoButton = view.findViewById(R.id.info_button) as Button
infoButton.setOnClickListener{
val fragment: InfoFragment = InfoFragment.newInstance(item)
val fragmentManager: FragmentManager = childFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
// this is where I have been having no success
// and know my approach is certainly wrong/confused
fragmentTransaction.replace(R.id.item_frame, fragment)
fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
}
return view
}
companion object {
fun newInstance(i: Item): ItemFragment {
return ItemFragment(i)
}
}
}
fragment_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/item_frame"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp">
<Button
android:id="#+id/info_button"
android:layout_gravity="center|bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/some_string"
/>
</FrameLayout>
InfoFragment.kt and fragment_info.xml could be anything, it doesn't matter what they are for the purposes of my question.
If I understand your problem correctly, the solution here is quite simple.
You have an Activity in which there is a FrameLayout, which is a container for fragments. Actually in it you put your fragment with ViewPager. When you click on the button, you simply put your new fragment on top of the fragment with ViewPager.
The bottom line is that all actions must occur through Activity. Those. you must have an interface that your Activity will implement, and the fragments will receive its instance from the context (for example, in the fragment's onAttach () method). When you click on your button, you simply pass your event to the Activity. And Activity is already creating a new fragment, thereby preserving the backstack.
It is important to understand that when working with fragments, they should not know anything about each other's existence. All operations must occur through abstraction and be performed by your parentView (Activity, ParentFragmen, etc.)
I was generated new activity + fragment named "adddebtinfo"
and try to start this activity using FAB in MainActivity but it doesn't work at all.
MainActivity.kt
var fab: FloatingActionButton = findViewById(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this, AddDebtInfoFragment::class.java)
startActivity(intent)
AddDebtInfoFragment.kt
private lateinit var viewModel: AddDebtInfoViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
var binding : AddDebtInfoActivityBinding = DataBindingUtil.inflate(inflater ,R.layout.add_debt_info_fragment,container , false)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(AddDebtInfoViewModel::class.java)
// TODO: Use the ViewModel
}
}
add_debt_info_fragment.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/add_debt_info_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.adddebtinfo.AddDebtInfoFragment">
add_debt_info_activity.xml
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".AddDebtInfo"/>
You are trying to start a Fragment using Intent. You should replace the AddDebtInfoFragment with the name of the activity in the following line:
val intent = Intent(this, AddDebtInfoFragment::class.java)
For add / replace fragment you need to perform FragmentTrascations on click of the FAB.
For open a new activity you need to perform startActivity(someIntent) with respect to FAB's activity / context.
Purusann, you want to use FragmentTransaction with the container FrameLayout you have.
fab.setOnClickListener {
// Create a new Fragment to be placed in the activity layout
val firstFragment = AddDebtInfoFragment()
// Add the fragment to the 'container' FrameLayout
supportFragmentManager.beginTransaction()
.add(R.id.container, firstFragment).commit()
}
Please read this Android doc page to understand how to use Fragments: https://developer.android.com/training/basics/fragments/fragment-ui