In my first fragment I have an EditText with a value. The problem is when I go to FragmentTwo and go back to FragmentOne the EditText (in FragmentOne) loses the text I previous inserted. I tried to save it in the onSaveInstanceState() and retrieve in the onViewStateRestored() but it seems that he does not store the information because the onSaveInstanceState() is never called and the savedInstanceState is always null.
This is my fragment replacing method
fragmentManager
?.beginTransaction()
?.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_left, R.anim.slide_out_right, R.anim.slide_in_right)
?.replace(frame, MyFragment.newInstance(), tag)
?.addToBackStack(tag)
?.commit()
This is my onSaveInstanceState() in FragmentOne:
override fun onSaveInstanceState(outState: Bundle) {
outState.putDouble(Constants.currentAmount, currentAmount)
super.onSaveInstanceState(outState)
}
This is my onViewRestored in FragmentOne:
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
if(savedInstanceState!= null) {
currentAmountSaved = savedInstanceState.getDouble(Constants.currentAmount)
}
}
This is my back press in FragmentTwo:
fragmentManager?.popBackStack()
So my goal is to save the EditText, pass to FragmentTwo, and when I go back to FragmentOne the same EditText is with the value I put when I change Fragments.
This is my xml file from the FragmentOne(The fragment I want to restore)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/contentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<View
android:id="#+id/fManageFundsHeaderLayout"
android:layout_width="match_parent"
android:layout_height="250dp"
android:background="#color/colorBackgroundToolbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" />
<include
android:id="#+id/addFundsComponent"
layout="#layout/component_manage_funds"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/fManageFundsHeaderLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
You can do this:
private var contentView: View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (contentView == null) {
contentView = inflater.inflate(xxxxx)
}
(contentView?.parent as? ViewGroup)?.removeView(contentView)
return contentView
}
I think that u r using wrong restore state callback. Try to explore Bundle object from onCreate/onCreateView methods of fragment
Related
I encountered a problem when using databinding and viewpager2. I used viewpager2 in a fragment. FragmentA in viewpager2 wanted to share the viewmodel of the fragment.
A TextView attribute text is bound in fragmentA, but postValue in viewmodel cannot change the text of TextView
But fragmentA is bound to a click event (TextView has a click, triggering the sendCode() function), which can be triggered in the viewmodel
fragmentA:
class SignUpMainFragment(val vm:SignUpFragmentVM):Fragment() {
private var mBinding:FragmentSignUpMainBinding?=null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_sign_up_main,container,true)
mBinding?.signUp = vm
return mBinding?.root!!
}
}
Layout of fragmentA:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="signUp"
type="com.xxx.SignUpFragmentVM" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="#+id/et_email"
android:layout_width="468dp"
android:layout_height="94dp"
android:layout_marginStart="40dp"
android:hint="Email"
android:text="#={signUp.txtEmail}"
android:textSize="30sp"
android:textColor="#color/color_aaa280"
android:drawableStart="#mipmap/img_mail"
android:paddingStart="26dp"
android:drawablePadding="18dp"
android:inputType="textEmailAddress"
android:background="#drawable/shape_edit_bg_e7e7e7"
app:layout_constraintTop_toTopOf="#id/et_name"
app:layout_constraintStart_toEndOf="#id/et_name" />
<TextView
android:id="#+id/tv_send_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#{signUp.txtSendEmail}"
android:textSize="30.5sp"
android:textColor="#color/color_71cbc0"
android:onClick="#{signUp.click}"
android:clickable="true"
android:layout_marginStart="25dp"
app:layout_constraintTop_toTopOf="#id/et_lock_psw"
app:layout_constraintBottom_toBottomOf="#id/et_lock_psw"
app:layout_constraintStart_toEndOf="#id/et_lock_psw"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
SignUpFragmentVM:
class SignUpFragmentVM constructor():ViewModel() {
val txtSendEmail = MutableLiveData("Get verified.")
....
private fun sendCode(){
viewModelScope.launch {
...
//TODO:60s Countdown
txtSendEmail.postValue("60")
}
}
}
You need more
mBinding?.signUp = vm
mBinding?.lifecycleOwner = this // => add
Why is val rv: RecyclerView = findViewById(R.id.material_list_rv) returning null?
MainActivity
class MainActivity : AppCompatActivity() {
var adaptor: MaterialListAdaptor = MaterialListAdaptor()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add<MaterialListFragment>(R.id.fragment_container_view)
}
}
initRecyclerView()
setRecyclerViewData()
}
private fun initRecyclerView(){
val rv: RecyclerView = findViewById(R.id.material_list_rv)
rv.layoutManager = LinearLayoutManager(this#MainActivity)
rv.adapter = adaptor
}
private fun setRecyclerViewData(){
val l = DataSource.createMaterialList()
adaptor.setDataSet(l)
}
}
activity_main
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MaterialListFragment
class MaterialListFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_material_list, container, false)
}
}
fragment_material_list
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MaterialListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/material_list_rv"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="#layout/material_list_rv_list_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
The problem
You placed the call to findViewById in MainActivity.onCreate. It's too early for that, the Fragment's view hasn't been created at that point.
You should be looking for the view in your MaterialListFragment. If you call findViewById in onCreateView, it will work. Like this:
val root = inflater.inflate(R.layout.fragment_material_list, container, false)
val rv = root.findViewById(R.id. material_list_rv)
// Do whatever (eg assign rv to a field in the Fragment to use it later)
return root
Some advice
Even if your code worked, try to have Activities, Fragments and custom Views manage their own children.
If the upper containers dive deep into their children, it's very easy to make mistakes (like the one here!) and you lose the ability to update internal details of Fragments and Views without breaking the containing Activity.
Activity/Fragment lifecycle chart
To orient yourself, keep this in mind (though this diagram is EXTREMELY simplified):
You are looking for RecylerView in MainActivity, where it does not exist.
Instead you should try to access it in FragmentActivity.
This is supposedly a simple thing to achieve but for some reason I can't get it to work. I'm using SDK 30 emulator to test. Basically the new fragment appears but there is zero animation whatsoever. If I add a Fade() transition it works only for the fade transition. I've tried setting sharedElement callback but the methods are called only intermittently. Basically it's a simple image. You click on it and the new fragment appears. Click it again and it's gone.
Layout snippet of activity containing image
<FrameLayout
android:id="#+id/container_test"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/white"
>
<ImageView
android:id="#+id/image_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_baseline_error_24"
android:transitionName="image"
android:layout_gravity="center"
/>
</FrameLayout>
Logic to open image fragment
override fun openImageFragment(imageView: ImageView) {
val fragment = ImageFragment()
fragment.image = imageView.drawable
fragment.arguments = Bundle().also {
it.putString(ImageFragment.KEY_TRANSITION, imageView.transitionName)
}
// fragment.enterTransition = Fade()
// fragment.exitTransition = Fade()
supportFragmentManager
.beginTransaction()
.setReorderingAllowed(true)
.addSharedElement(imageView, imageView.transitionName)
.replace(R.id.container_test, fragment)
.commitNow()
}
Image fragment layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/black">
<ImageView
android:id="#+id/image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
/>
</FrameLayout>
ImageFragment
class ImageFragment : Fragment() {
private lateinit var binding: FragmentImageBinding
var image: Drawable?= null
companion object {
const val KEY_TRANSITION = "TRANSITION"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val transition = TransitionInflater.from(requireContext()).inflateTransition(android.R.transition.move)
sharedElementEnterTransition = transition
sharedElementReturnTransition = transition
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentImageBinding.inflate(inflater, container, false)
binding.imageView.transitionName = arguments?.getString(KEY_TRANSITION)
binding.imageView.setImageDrawable(image)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setOnClickListener {
requireActivity().supportFragmentManager
.beginTransaction()
.remove(this)
.commit()
}
}
}
After much trial and error, I figured out the reason.
My initial fragment was placed statically on the XML, which prevented the fragment manager from replacing it. By using the layout inspector I figured out that the new fragment was placed right below the first one.
Basically the solution is to add the first fragment programmatically so the fragment manager can manipulate it.
I am trying to show a custom dialog from a fragment. When it is about to be displayed the screen gets darker, but no dialog is presented. I also tried to show it from the activity containing the fragment, but I get the same result.
Here is the layout for my dialog:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="239dp"
android:background="#00f"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="210dp"
android:layout_marginStart="20dp"
android:layout_marginLeft="20dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:background="#drawable/pop_up_shape"
android:backgroundTint="#00ff00"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
This is my dialog fragment class:
class AddPlaylistPopUp: DialogFragment(){
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.add_playlist_popup_layout, container, true)
}
}
and this is the method that should open the dialog in the activity:
fun testBut(view: View) {
val fm: FragmentManager = supportFragmentManager
val editNameDialogFragment = AddPlaylistPopUp()
editNameDialogFragment.show(fm, "fragment_edit_name")
}
in the fragment I tried like this:
fun openDialog(){
val fm = fragmentManager!!
val editNameDialogFragment = AddPlaylistPopUp()
editNameDialogFragment.show(fm, "fragment_edit_name")
}
This is the guide I followed:
https://guides.codepath.com/android/Using-DialogFragment#passing-data-to-parent-fragment
You are not creating any Dialog, you should be overriding onCreateDialog instead.
class AddPlaylistPopUp: DialogFragment(){
override fun onCreateDialog(
savedInstanceState: Bundle?
): Dialog {
val builder = AlertDialog.Builder(getContext())
builder.setTitle("Title")
builder.setView(R.layout.add_playlist_popup_layout)
return builder.create()
}
}
This way you don't event need to implement onCreateView(LayoutInflater, ViewGroup, Bundle) and you have much more control on the dialog creation.
If you would like to stick with onCreateView approach, you could fix your issue by changing the third parameter of function call LayoutInflater.inflate(resource, root, attachToRoot) by setting it to false.
EDIT:
To keep match_parent width, override Dialog size.
In my application I'm trying to create 2 fragments with ViewPagers. The fragments have 3 tabs/pages each with RecyclerViews and they are supposed to be backed by a database, so I'm keeping them in a List.
class MainActivity : AppCompatActivity() {
val fragments = listOf(SwipeFragment(), SwipeFragment())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[0]).commit()
button1.setOnClickListener {
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[0]).commit()
}
button2.setOnClickListener {
supportFragmentManager.beginTransaction().replace(R.id.placeholder, fragments[1]).commit()
}
}
}
The problem is that after navigating away from the SwipeFragment and going back, the view seems empty. Then, if you navigate to e.g. the leftmost page, the rightmost one "appears" (when you go to it).
This results in the middle page staying empty. (the reason for it being that FragmentStatePagerAdapter keeps only the current page and 2 adjacent ones by default. The middle one doesn't get refreshed in a layout with 3 tabs - I tried it also with 5 tabs and I'm able to bring all of them back by going back and forth).
After some debugging I saw that the fragments that represent pages don't get removed from the FragmentManager, but the main SwipeFragment is. I can't get my head around how FragmentManager really works, even after reading almost all of the source code.
If I were able to somehow safely remove the fragments from the FragmentManager, it may work.
I've tried some techniques for saving state, but the framework does that automatically (which might actually be the cause of this problem).
I'm just a beginner in Android, so there's probably a better way to do this anyway. I'll leave the rest of the files here for reference.
activity_main.xml
<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"
android:orientation="vertical"
tools:context="com.example.test.MainActivity">
<FrameLayout
android:id="#+id/placeholder"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="0"
android:orientation="horizontal">
<Button
android:id="#+id/button1"
style="?android:buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="button1" />
<Button
android:id="#+id/button2"
style="?android:buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="button2" />
</LinearLayout>
</LinearLayout>
SwipeFragment.kt
class SwipeFragment : Fragment() {
// TODO: set up and keep the database here
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater!!.inflate(R.layout.fragment_swipe, container, false)
view.tab_layout.setupWithViewPager(view.pager)
view.pager.adapter = DummyPagerAdapter(activity.supportFragmentManager, index)
return view
}
}
fragment_swipe.xml
<android.support.v4.view.ViewPager
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:id="#+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.test.SwipeFragment">
<android.support.design.widget.TabLayout
android:id="#+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.v4.view.ViewPager>
ItemListFragment.kt
class ItemListFragment() : Fragment() {
// Or keep the database here?
// Probably not the best idea though
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater!!.inflate(R.layout.fragment_item_list, container, false)
if (view is RecyclerView) {
view.layoutManager = LinearLayoutManager(view.context)
view.adapter = MyItemRecyclerViewAdapter(DummyContent.ITEMS)
}
return view
}
}
DummyPagerAdapter.kt
class DummyPagerAdapter(manager: FragmentManager, val parentIndex: Int) :
FragmentStatePagerAdapter(manager) {
override fun getItem(position: Int): Fragment = ItemListFragment()
override fun getPageTitle(position: Int): CharSequence = "${ position + 1 }"
override fun getCount(): Int = 3
}
And a basic implementation of RecyclerView.Adapter generated by Android Studio
MyItemRecyclerViewAdapter.kt
class MyItemRecyclerViewAdapter(val values: List<DummyItem>) :
RecyclerView.Adapter<MyItemRecyclerViewAdapter.ViewHolder>() {
...
}
fragment_item_list.xml
<android.support.v7.widget.RecyclerView 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:id="#+id/list"
android:name="com.example.test.ItemListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
tools:context="com.example.test.ItemListFragment"
tools:listitem="#layout/fragment_item" />
You should use childFragmentManager instead of activity.supportFragmentManager inside SwipeFragment.
view.pager.adapter = DummyPagerAdapter(childFragmentManager, index)
The difference between getSupportFragmentManager() and getChildFragmentManager() is discussed here.
Basically, the difference is that Fragment's now have their own internal FragmentManager that can handle Fragments. The child FragmentManager is the one that handles Fragments contained within only the Fragment that it was added to. The other FragmentManager is contained within the entire Activity.
In my case childFragmentManager didn't help. I think we can use a pattern "Observer" to update existing fragments of ViewPager with new data when return from another fragment.