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()
Related
I create ViewModel class at fragment, but viewModel is not saving after rotation - every time i got new Viewmodel instance. Where is problem?
Acrivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.addToBackStack(null)
.replace(R.id.main_container, VideoFragment())
.commit()
}
}
Fragment:
class VideoFragment: Fragment() {
lateinit var viewModel: VideoViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProvider(this).get(VideoViewModel::class.java)
return inflater.inflate(R.layout.fragment_video, container, false)
}
}
ViewModel:
class VideoViewModel: ViewModel() {
init {
Log.i("XXX", "$this ")
}
}
if i will use "requireActivity()" - as ViewModelStoreOwner - viewModel isnt recreate, but its will bound to activity lifecycle.
viewModel = ViewModelProvider(requireActivity()).get(VideoViewModel::class.java)
This is because you are replacing your Fragment on every configuration change when the Activity is recreated. The FragmentManager already retains your Fragment for you. You should commit the transaction only on the initial creation:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.addToBackStack(null)
.replace(R.id.main_container, VideoFragment())
.commit()
}
}
}
Your problem is most likely caused due to the activity being destroyed and then created again after app is rotated.
To fix this you can give your fragment an id/tag when navigating and then when activity is rotated call your supportFragmentManager if there already exists an instance of your old fragment, if it does, navigate to old fragment instance, otherwise create a new instance just like you are doing now.
Wax911 (commented on 9 may 2020) answer to this question:
https://github.com/InsertKoinIO/koin/issues/693
He explains the problem with activities and their lifecycle when rotating the screen
I have the following method
// Change fragment extension function to handle navigation easily
fun changeFragment(fragmentManager: FragmentManager?, #IdRes containerId: Int, fragment: Fragment?, addToBackStack: Boolean = false) {
if (fragmentManager == null || fragment == null) return
val fragmentTransaction = fragmentManager.beginTransaction()
if (addToBackStack) fragmentTransaction.addToBackStack(null)
fragmentTransaction.replace(containerId, fragment, fragment::class.java.simpleName).commit()
}
and I am using it inside one of my fragments -
class InspectionFragment : Fragment(R.layout.fragment_inspection) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
inspectionAdapter = InspectionAdapter { inspection ->
changeFragment(
parentFragmentManager, R.id.fragment_inspection_frame_layout,
InspectionDetailsFragment.newInstance(inspection), true
)
}
}
}
class InspectionDetailsFragment : Fragment() {
companion object {
fun newInstance(inspection: Inspection): InspectionDetailsFragment {
val inspectionDetailsFragment = InspectionDetailsFragment()
val bundle = Bundle()
bundle.putParcelable(GeneralConstants.INSPECTION, inspection)
inspectionDetailsFragment.arguments = bundle
return inspectionDetailsFragment
}
}
}
The thing is that, when I instansiate onetop of the 'InspectionDetailsFragment' layer another fragment and press back, it goes directly back to the 'InspectionFragment' parent.
I can't understand why this is happening.
Anyone has an idea?
When you call changeFragment function, you're passing true as last argument:
changeFragment(
parentFragmentManager, R.id.fragment_inspection_frame_layout,
InspectionDetailsFragment.newInstance(inspection), true
)
In changeFragment function:
if (addToBackStack) fragmentTransaction.addToBackStack(null)
It doesn't get added to backStack. Instead of null i think you should pass String value as the name of the backStack you want to use.
I'm not 100% sure about this. You can read more about fragment transactions from here: https://developer.android.com/guide/fragments/transactions
I have the weirdest bug on Kotlin, and after two days of trying I finally asking for help.
The problem is simple : I have two fragment and one activity, the first fragment A is a form, with a validate button, when I click on validate, the fragment B replace the fragment A, and if I press back, the fragment A show up again with the form filled.
My problem is that after the fragment is shown again, I can click on the button but the listener is not call, so I can't go to the fragment B again. The strange thing is that the other listener are properly working, so I'm thinking it's because the previous fragment is catching the onClick, but idk what to do. Here is some code :
ViewUtils :
fun addFragment(activity: Activity, fragment: androidx.fragment.app.Fragment, container: Int) {
val fragmentManager = (activity as AppCompatActivity).supportFragmentManager
val pendingTransaction = fragmentManager.beginTransaction().add(container, fragment, fragment.javaClass.name)
pendingTransaction.commitAllowingStateLoss()
}
fun replaceFragment(manager: FragmentManager, fragment: androidx.fragment.app.Fragment, container: Int) {
if (fragment.isAdded) return
val pendingTransaction = mangaer.beginTransaction()
pendingTransaction.replace(container, fragment, fragment.javaClass.name)
pendingTransaction.commitAllowingStateLoss()
}
fun removeFragment(activity: Activity, fragment: Fragment) {
val manager = (activity as AppCompatActivity).supportFragmentManager
val trans = manager.beginTransaction()
trans.remove(fragment)
trans.commit()
manager.popBackStack()
}
Activity :
fun displayFragmentA() {
ViewUtils.replaceFragment(supportFragmentManager, FragmentA,
R.id.fragmentLayout)
}
fun FragmentB() {
ViewUtils.replaceFragment(supportFragmentManager, FragmentB,
R.id.fragmentLayout)
}
Fragment A
class AFragment : BaseFragment(), AContract.View {
companion object {
#JvmStatic
fun newInstance(): AFragment {
val fragment = AFragment()
return fragment
}
}
#Inject
lateinit var APresenter: AContract.Presenter<AContract.View>
//end region
//region lifecycle
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_A_layout, container, false)
}
override fun onResume() {
super.onResume()
button_validate.setOnClickListener {
presenter.goToNextStep()
}
}
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
}
The listener was set in the onViewCreated but I tried moving it to onResume (didn't change anything)
Fragment B code is not important I think, but I can add it if it helps.
Any help is welcome, I really don't know what's going on, the replace/add methods were there before I came to the project, they are not perfect but they are working elsewhere on the project.
I try using breakpoint, the button is not null but we never enter the listener.
Edit : I tried on 3 differents devices, I don't have the bug with a Sony Android 9, but with Huawei et One plus 6 Android 10, the problem persist ..
Ok so after asking to a lot of people, the only solution I found is not using kotlin.synthetic, and using findById instead :
view.findViewById<Button>(R.id.button_validate.setOnClickListener
i am currently programming an app in Android Studio and i am having a big issue. The main problem is, that i want an activity with a fragment in it and this fragment has got a spinner. I wanted to find the spinner by id, but it always returned null and i read that i can't use findViewById if it is not in the ContentView i just set. So i am currently trying to find the fragment that contains the spinner, but i also can't find the fragment, i tried findFragmentById and findViewById from the FragmentManager. I always get a TypeCastException and if i try findFragmentById(...)!! it throws a NullPointer.
This is my MainActivity:
class MainActivity : AppCompatActivity() {
private val manager: FragmentManager? = supportFragmentManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
showDeviceFragment()
val fragment = manager!!.findFragmentById(R.id.fragment_holder) as DeviceFragment
val options = arrayOf("Wandhalterung (Arm)", "Gestellhalterung (Arm)", "Gestellhalterung")
fragment.option.adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, options)
}
fun showDeviceFragment() {
val transaction = manager!!.beginTransaction()
val fragment = DeviceFragment()
transaction.add(R.id.fragment_holder, fragment, "DEVICE_FRAGMENT")
transaction.addToBackStack(null)
transaction.commit()
}
}
And this is the DeviceFragment:
class DeviceFragment : Fragment() {
lateinit var option : Spinner
companion object {
fun newInstance(): DeviceFragment {
return DeviceFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_device, container, false)
option = view.spinner
return inflater?.inflate(R.layout.fragment_device, container, false)
}
}
The fragment_holder just is a FrameLayout.
Thanks in advance
Few things:
1) No need to hold reference for supportFragmentManager, use it directly (because i am not sure if it will be null when Activity is initialized)
2) Try removing addToBackStack(null) and using findFragmentByTag("DEVICE_FRAGMENT")
3) Most importantly, Don't try to access "things" of Fragment from Activity, do those Adapter initialization/fill in the Fragment itself. Because Fragment has its own lifecycle and you may try to access "things" at wrong lifecycle
So I have an activity with DrawerLayout. I can switch to Fragment1 which contains TabLayout with three other fragments (NestedFragment1, NestedFragment2, NestedFragment3). I want to be able to save data in each NestedFragments since I make API calls inside them. I tried to override onSaveInstanceState and save the data but SavedInstanceState was always null.
Fragment1 code:
class Fragment1 : Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var tabLayout: TabLayout
private lateinit var pagerAdapter: PagerAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pagerAdapter = ForecastPagerAdapter(childFragmentManager)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_forecast, container, false)
viewPager = view.findViewById(R.id.view_pager)
viewPager.offscreenPageLimit = 3
viewPager.adapter = pagerAdapter
tabLayout = view.findViewById(R.id.tabs)
tabLayout.setupWithViewPager(viewPager)
return view
}
}
Example of nested fragment code:
class NestedFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var recyclerViewAdapter: HourForecastAdapter
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
recyclerViewAdapter = HourForecastAdapter()
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater!!.inflate(R.layout.fragment_day_forecast, container, false)
swipeRefreshLayout = view.findViewById(R.id.swipe_refresh_layout_hour)
swipeRefreshLayout.apply {
this.setOnRefreshListener {
performQuery()
}
}
recyclerView = view.findViewById(R.id.recycler_view_hour_forecast)
recyclerView.adapter = recyclerViewAdapter
recyclerView.layoutManager = LinearLayoutManager(activity)
return view
}
fun performQuery() {
// ...
recyclerViewAdapter.forecastList = result.hourly.data
recyclerViewAdapter.notifyDataSetChanged()
}
}
I want to save forecastList (List) which is returned by performQuery method. Every time I switch to Fragment 1 all data from nested fragments is gone.
This is the code used for switching fragments:
navigationView.setNavigationItemSelectedListener {
var fragment = when (it.itemId) {
R.id.nav_map -> Fragment0()
R.id.nav_forecast -> Fragment1()
R.id.nav_app_info -> Fragment2()
else -> Fragment1()
}
replaceFragment()
it.isChecked = true
drawerLayout.closeDrawers()
true
}
}
Edit: added function for fragment replacement:
fun replaceFragment(fragment: android.support.v4.app.Fragment){
val fragmentName = fragment::class.java.simpleName
val isFragmentInBackStack = fragmentManager.popBackStackImmediate(fragmentName, 0)
if (!isFragmentInBackStack) {
fragmentManager.beginTransaction()
.replace(R.id.main_activity_frame, fragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack(fragmentName)
.commit()
}
}
First, you have to call .addToBackstack(null) before you commit your transaction. This will affect your back-button also.
See #documentation:
Add this transaction to the back stack. This means that the transaction will be remembered after it is committed, and will reverse its operation when later popped off the stack.
Why this works this way? Your application doesn't known you want to open old fragment instead of creating new one (because you actually asked for creating new fragment). Also, you have to tell your application - "Ey, you! Don't create new fragment, but get the old one from backstack if there is any".
You can check how to do it fe. here: How to resume Fragment from BackStack if exists
PS: It's good practice to creating fragment not by constructor, but by static method "getInstance(): Fragment".
So I managed to solve this problem by using fragment "caching":
fun cacheFragment(fragment: android.support.v4.app.Fragment) {
Log.d("Caching", "Working with ${fragment.javaClass.simpleName}")
var findFragment = supportFragmentManager.findFragmentByTag(fragment.javaClass.simpleName)
if (findFragment == null) {
Log.d("Caching", "Creating new instance of ${fragment.javaClass.simpleName}")
findFragment = fragment.javaClass.newInstance()
}
supportFragmentManager.beginTransaction()
.replace(R.id.main_activity_frame, findFragment, fragment.javaClass.simpleName)
.addToBackStack(null)
.commit()
}
When a user selects an item from nav bar, we check if the fragment already exists in the back stack and then we can reuse it.