Is there any way to make a shared transition between fragment1 inside activity1 and fragment2 inside activity2?
I have tried achieving this like so:
val intent = Intent(this, RecipeActivity::class.java)
intent.putExtra("recipeId", recipeId)
val elem1 =
Pair<View, String>(itemView.findViewById(R.id.recipe_preview), "preview")
val elem2 =
Pair<View, String>(itemView.findViewById(R.id.recipe_title), "title")
val elem3 =
Pair<View, String>(itemView.findViewById(R.id.recipe_rating_stars), "rating_stars")
val elem4 =
Pair<View, String>(itemView.findViewById(R.id.recipe_rating), "rating")
val elem5 =
Pair<View, String>(itemView.findViewById(R.id.recipe_description), "description")
val elem6 =
Pair<View, String>(itemView.findViewById(R.id.author_avatar), "avatar")
val options =
ActivityOptionsCompat.makeSceneTransitionAnimation(
this, elem1, elem2, elem3, elem4, elem5, elem6
)
startActivity(intent, options.toBundle())
But that didn't work so well. Do I have to redesign my app so those two fragments will be inside a single activity or is there any workaround? Thank you
The idea: Pause the transaction until the targed fragment is fully loaded, created and is about to be drawn. Then continue the transaction.
The code:
Everything you do in your first activity is ok and we won't touch it.
The first thing your activity has to is to stop the transaction. Therefore you need to call supportPostponeEnterTransition() in onCreate() of your second activity. This will tell android to wait with the transaction until you tell it to start it.
Secondly you need to know when the fragment is about to be drawn. In my use case I display some fragments in a ViewPager what makes things a lot easier, as you can add an ViewTreeObserver to it and wait until the ViewPager is loaded because you know that at this point the fragments are already created and basically drawn even if you can't see them. When using frgaments the normal way with transaction you need some trickery.
Important: Everything from now on is not tested but in theory it should work.
In your fragment you do something like this:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
Instead you to do it like this:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
return view
}
You need the root view of your fragment because we will add the ViewTreeObserver to it.
But before you to that you need to create an interface in your FragmentClass or add the method to your preexisting one:
interface FragmentListener {
fun resumeTransaction()
}
You need to implement it in your activity:
override fun resumeTransaction() {
supportStartPostponedEnterTransition()
}
In your fragment we need to get the Activity as listener. In the onAttach do following:
try {
// Instantiate the FragmentListener so we can send the event to the host
listener = context as FragmentInterface
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException((context.toString() + " must implement FragmentInterface"))
}
Now we get back to the ViewTreeObserver. In the onCreateView you do this:
viewPager.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
listener.resumeTransaction()
}
}
)
If I didn't forget anything this should work. If not please tell me, I will then try to make an example app later this day as I don't have more time now.
Related
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
I need to make a custom behaviour, when the user press the back button then the user will go to certain destination programatically. I actually have read this Handling back button in Android Navigation Component
but I don't understand how to use that custom back button code.it seems weird to me.
I have tried using this code below
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentView = inflater.inflate(R.layout.fragment_search_setting, container, false)
// set custom back button
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
// navigate to certain destination
Navigation.findNavController(fragmentView).popBackStack(R.id.destination_create_event, false)
}
return fragmentView
}
but I get type mismatch error like this
You must create new Instance of the OnBackPressedCallback abstract class and implement its abstract method .
I hope this helps you:
val callback = requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true){
override fun handleOnBackPressed() {
Navigation.findNavController(fragmentView).popBackStack(R.id.destination_create_event, false)
}
})
// The callback can be enabled or disabled here or in the lambda
}
I am trying to use the Navigation Component with a retained Fragment and an enter animation. The fragment appears with the animation as expected. On rotating my device I get the following crash:
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
I can solve this by removing the enter animation (from the nav graph) or by making the fragment not be retained.
Does anyone have a clue how to solves this as I wanted both retained and the animation.
Here is my onCreate:
private var binding: FragmentDemoBinding? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? {
if (binding == null) {
binding = FragmentDemoBinding.inflate(inflater, container, false)
}
retainInstance = true
return binding?.root
}
edit: Actually on reading a bit more I am not sure it makes sense to try and use retained fragments within the navigation component. If I need to persist data on configuration changes I guess a headless fragment or use onSaveInstance or ViewModel component etc.
In onDestroyView method remove all views or just animation view.
override fun onDestroyView() {
if(view != null){
val parent = view.parent
parent.removeAllViews
}
super.onDestroyView()
}
I'm starting bottomSheetDialogFragment from a fragment A.
I want to select the date from that bottomSheetDialogFragment then set it in the fragment A.
The select date is already done, I just want to get it in the fragment A to set it in some fields.
How can I get the value?
Any suggestions how to do it?
Create an interface class like this
public interface CustomInterface {
public void callbackMethod(String date);
}
Implement this interface in your Activity or Fragment. and make an object of this Interface.
private CustomInterface callback;
Initialize it in onCreate or onCreateView
callback=this;
Now pass this callback in your BottomSheetDialogFragment constructor when you call it.
yourBottomSheetObject = new YourBottomSheet(callback);
yourBottomSheetObject.show(getSupportFragmentManager()," string");
Now in your BottomSheetFragment's constructor
private CustomInterface callback;
public SelectStartTimeSheet(CustomInterface callback){
this.callback=callback;
}
And at last use this callback object to set your date
callback.callbackMethod("your date");
and yout will recieve this date in your Fragment or Your Activity in callbackMethod function.
override the constructor of a fragment is a bad practice as the document said:
Every fragment must have an
* empty constructor, so it can be instantiated when restoring its
* activity's state.
if you using another constructor that passing a callback as the param, when the fragment is resotored by the framework, your app crash
the recommend way is using viewModel and livedata.
Android navigation architecture component
eg:
Suppose you open Fragment B from Fragment A using navController.
and you want some data from fragment B to Fragment A.
class B :BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.your_layout, container, false)
root.sampleButton.setOnClickListener {
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("your_key", "your_value")
dismiss()
}
}
and in your Fragment A:
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>("your_key")
?.observe(viewLifecycleOwner) {
if (it == "your_value") {
//your code
}
}
you can use do as below:
Select Account Fragment code
class SelectAccountFragment(val clickListener: OnOptionCLickListener) : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.bottom_fragment_accounts, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val list = DataProcessorApp(context).allUsers
val rvAccounts = view.findViewById<RecyclerView>(R.id.rvAccounts)
rvAccounts.layoutManager = LinearLayoutManager(context)
rvAccounts.adapter = AccountsAdapter(context, list)
Log.e(tag,"Accounts "+list.size);
tvAccountAdd.setOnClickListener {
val intent = Intent(context,LoginActivity::class.java)
startActivity(intent)
}
tvManageAccounts.setOnClickListener {
Log.e(tag,"Manage Click")
clickListener.onManageClick()
}
}
interface OnOptionCLickListener{
fun onManageClick()
}
}
Now show and get call back into another fragment /activity as below
SelectAccountFragment accountFragment = new SelectAccountFragment(() -> {
//get fragment by tag and dismiss it
BottomSheetDialogFragment fragment = (BottomSheetDialogFragment) getChildFragmentManager().findFragmentByTag(SelectAccountFragment.class.getSimpleName();
if (fragment!=null){
fragment.dismiss();
}
});
accountFragment.show(getChildFragmentManager(),SelectAccountFragment.class.getSimpleName());
If you are using BottomSheetDialogFragment , since it's a fragment, you should create your interface and bind to it at onAttach lifecycle method of the fragment , doing the appropriate cast of activity reference to your listener/callback type.
Implement this interface in your activity and dispatch change when someone click in a item of fragment's inner recyclerview, for instance
It's a well known pattern and are explained better at here
One big advice is rethink your app architecture, since the best approach is to always pass primitive/simple/tiny data between Android components through Bundle, and your components are able to retrieve the required state with their dependencies later on.
For example, you should never pass along large Objects like Bitmaps, Data Classes , DTO's or View References.
first there is some serialization process going on regarding Parcel which impacts in app responsiveness
second it can lead you to TransactionTooLarge type of error.
Hope that helps!
You can also use LocalBroadcastManager. And as hglf said, it is better to keep the empty constructor for your fragment and use newInstance(Type value) instead to instantiate your fragment if you still want to use the interface callBack way.
You can use the benefit of Navigation library:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = findNavController();
// After a configuration change or process death, the currentBackStackEntry
// points to the dialog destination, so you must use getBackStackEntry()
// with the specific ID of your destination to ensure we always
// get the right NavBackStackEntry
val navBackStackEntry = navController.getBackStackEntry(R.id.your_fragment)
// Create our observer and add it to the NavBackStackEntry's lifecycle
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains("key")) {
val result = navBackStackEntry.savedStateHandle.get<String>("key");
// Do something with the result
}
}
navBackStackEntry.lifecycle.addObserver(observer)
// As addObserver() does not automatically remove the observer, we
// call removeObserver() manually when the view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
For more info, read the document.
The accepted answer is wrong.
What you can do is just user Fragment A's childFragmentManager when calling show().
like this:
val childFragmentManager = fragmentA.childFragmentManager
bottomSheetDialogFragment.show(childFragmentManager, "dialog")