How to refresh data in fragment in ViewPager2 with an interface - android

I'm trying to refresh the data in my fragment every time the user clicks on it in the Bottom Menu Navigation. I already wrote an interface which gets called each time the fragment gets selected by the user
The problem is that the method inside my fragment has no access to the view of the fragment (I guess):
MainMenu
viewPager = findViewById(R.id.frame_container)
viewPager!!.offscreenPageLimit = 5
viewPager!!.orientation = ViewPager2.ORIENTATION_HORIZONTAL
viewPager!!.adapter = pageAdapter
viewPager!!.currentItem = 0
viewPager!!.isUserInputEnabled = false
viewPager!!.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if(position == 2) {
val mFragment = ListFragment()
mFragment.ready(this#MainMenu)
}
}
})
The Interface "Ready":
interface ReadyInterface {
fun ready(activity: FragmentActivity?)
}
and the ListFragment:
class ListFragment: Fragment(), ReadyInterface {
var mView : View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_list, container, false)
this.mView = view
val user = FirebaseAuth.getInstance().currentUser
return if (user != null) {
view
} else {
val unregisteredView = inflater.inflate(R.layout.fragment_unregistred, container, false)
val registerNow = unregisteredView.findViewById<TextView>(R.id.textview_registernow)
unregisteredView
}
override fun ready(activity: FragmentActivity?) {
testText = mView!!.findViewById(R.id.test_text)
testText.text = "Test Text here"
Toast.makeText(activity!!.applicationContext,"Test",Toast.LENGTH_LONG).show()
}
This code here crashes with a "kotlin.KotlinNullPointerException" on line "testText.text = ...."
So I guess the fun ready hasn't got access to the view of my fragment because of the fragments lifecycle, am I right? How could I fix this?

There is no need to implement interface here. You can directly make ready() a member function of ListFragment and call it from onPageSelected(position: Int) in MainMenuActivity. Currently, the interface method is being called before onCreateView() causing it to throw null pointer exception as the view is not initialised yet.

You are creating a new instance of ListFragment() while calling ready on it. This new fragment is not attached to the ViewPager. This new instance also has not gone through any lifecycle methods, therefore, it's view is not yet initialized. Due to this, you get a NullPointerException while calling methods on a view.
To solve this you can instead fetch the current fragment from the ViewPager and call your method on it.

Related

Should I inflate the layout in onCreateView or onViewCreated?

I am using the following fragment to show an onboarding screen on the first launch of the application. Should I inflate my layout in onCreateView or in onViewCreated? I don't quite understand how to decide on this. Also, do I need to create a ViewModel for my code?
class OnBoardingFragment : Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var dotsLayout: LinearLayout
private lateinit var sliderAdapter: SliderAdapter
private lateinit var dots: Array<TextView?>
private lateinit var letsGetStarted: Button
private lateinit var next: Button
private lateinit var animation: Animation
private var currentPos: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navOptions = NavOptions.Builder().setPopUpTo(R.id.onBoardingFragment, true).build()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_onboarding, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.slider);
dotsLayout = view.findViewById(R.id.dots);
letsGetStarted = view.findViewById(R.id.get_started_btn);
next = view.findViewById(R.id.next_btn)
sliderAdapter = SliderAdapter(requireContext())
viewPager.adapter = sliderAdapter;
addDots(0);
viewPager.addOnPageChangeListener(changeListener);
next.setOnClickListener {
viewPager.currentItem = currentPos + 1
}
letsGetStarted.setOnClickListener {
findNavController().navigate(R.id.action_onBoardingFragment_to_loginFragment)
}
}
private fun addDots(position: Int) {
dots = arrayOfNulls(2)
dotsLayout.removeAllViews();
for (i in dots.indices) {
dots[i] = TextView(requireContext())
dots[i]!!.text = HtmlCompat.fromHtml("•", HtmlCompat.FROM_HTML_MODE_LEGACY)
dots[i]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
android.R.color.darker_gray
)
)
dots[i]!!.textSize = 35F
dotsLayout.addView(dots[i])
}
if (dots.isNotEmpty()) {
dots[position]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.wine_red
)
)
}
}
private var changeListener: ViewPager.OnPageChangeListener =
object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
addDots(position)
currentPos = position
animation =
AnimationUtils.loadAnimation(requireContext(), android.R.anim.fade_in)
if (position == 0) {
letsGetStarted.visibility = View.INVISIBLE
next.animation = animation
next.visibility = View.VISIBLE
} else {
letsGetStarted.animation = animation
letsGetStarted.visibility = View.VISIBLE
next.visibility = View.INVISIBLE
}
}
override fun onPageScrollStateChanged(state: Int) {}
}
}`
The Android framework calls Fragment's onCreateView to create the view object hierarchy. Therefore, it's correct to inflate the layout here as you did.
onViewCreated is called afterwards, usually you find views and setup them. So, your code is ok.
Regarding the ViewModel, in your sample code you're just configuring the UI so you won't need it. If instead, you need to obtain some data from an API service, transform it, show the states of "loading data", "data retrieved" and "there was an error retrieving data", then you would like not to do those things in the fragment and you could consider using an MVVM approach.
Some references:
https://developer.android.com/guide/fragments/lifecycle#fragment_created_and_view_initialized
https://guides.codepath.com/android/Creating-and-Using-Fragments
https://developer.android.com/topic/architecture
onCreateView is where you inflate the view hierarchy, and return it (so the Fragment can display it). If you're handling that inflation yourself, you need to override onCreateView so you can take care of it when the system makes that request. That's why it's named that way - when the view (displayed layout) is being created, this function is called, and it provides a View.
onViewCreated is called after the Fragment's view has already been created and provided to it for display. You get a reference to that view passed in, so you can do setup stuff like assigning click listeners, observing View Models that update UI elements, etc. You don't inflate your layout here because it won't be displayed (unless you're explicitly inflating other stuff and adding it to the existing view for some reason, which is more advanced and probably not what you're talking about).
So onCreateView is really concerned with creating a view hierarchy for display, and onViewCreated is for taking that displayed hierarchy and initialising your stuff. You might not need to implement onCreateView at all (e.g. if you use the Fragment constructor that takes a layout ID, so it sets it up for you) in which case you'd just implement onViewCreated instead. Or if you are handling it yourself in onCreateView, and you don't have much setup code, you might run that on the View you've inflated before you return it, and not bother with onViewCreated at all.
It's worth getting familiar with the Fragment lifecycle if you haven't already, just so you know the basic way the system moves between states and the callbacks it calls as it does so (and have a look at the documentation for the callback methods too!)

ViewPager2 how to restore fragments when navigating back

Hi folks I have a ViewPager2 with single activity architecture. When I click a button, I swap out the ViewPager2 host fragment with another one using the Jetpack Navigation library.
This calls onDestroyView for the host fragment. When I click back, we are back to onCreateView. How can I return to the ViewPager2 I was looking at, seeing as the host fragment itself is not destroyed?
I believe based on this answer that restoring a ViewPager2 is actually impossible, not sure if this is by design or not. So what is the best practice here, assuming each fragment loads a heavy list, am I supposed to reload all the data every time a user pops the backstack into my viewpager? The only thing I can think of is to have an activity scoped ViewModel which maintains the list of data for each fragment, which sounds ridiculous, imagine if my pages were dynamically generated or I had several recycler views on each fragment....
Here is my attempt, I am trying to do the bare minimum when navigating back, however without assigning the view pager adapter again, I am looking at a blank fragment tab. I don't understand this, the binding has not died, so why is the view pager not capable of restoring my fragment?
OrderTabsFragment.kt
var adapter: TabsPagerAdapter? = null
private var _binding: FragmentOrdersTabsBinding? = null
private val binding get() = _binding!!
private var initted = false
override fun onCreate(savedInstanceState: Bundle?) {
Timber.d("OrderTabsFragment $initted - onCreate $savedInstanceState")
super.onCreate(savedInstanceState)
adapter = TabsPagerAdapter(this, Tabs.values().size)
adapter?.currentTab = Tabs.valueOf(savedInstanceState?.getString(CURRENT_TAB) ?: Tabs.ACTIVE.name)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
Timber.d("OrderTabsFragment $initted - onCreateView $savedInstanceState, _binding=$_binding")
if(_binding == null)
_binding = FragmentOrdersTabsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Timber.d("OrderTabsFragment $initted - onViewCreated $savedInstanceState")
super.onViewCreated(view, savedInstanceState)
if(!initted) {
initted = true
val viewpager = binding.viewpager
viewpager.adapter = adapter
viewpager.isSaveEnabled = false
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {}
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {
if (adapter?.currentTab == Tabs.FILTERED) {
showFilterBalloon(tab)
}
}
})
TabLayoutMediator(binding.tabLayout, viewpager) { tab, position ->
when (position) {
0 -> tab.text = getString(R.string.title_active).uppercase(Locale.getDefault())
1 -> tab.text =
getString(R.string.title_scheduled).uppercase(Locale.getDefault())
2 -> tab.text =
getString(R.string.title_complete).uppercase(Locale.getDefault())
}
}.attach()
}
else{
val viewpager = binding.viewpager
viewpager.adapter = adapter //Required otherwise we are looking at a blank fragment tab. The adapter rv was detached and can't be reattached?
viewpager.isSaveEnabled = false //Required otherwise "Expected the adapter to be 'fresh' while restoring state."
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Timber.d("OrderTabsFragment $initted - onSaveInstanceState")
outState.putString(CURRENT_TAB, adapter?.currentTab?.name)
}
override fun onDestroy() {
super.onDestroy()
Timber.d("OrderTabsFragment $initted - onDestroy")
binding.viewpager.adapter = null
_binding = null
adapter = null
}
enum class Tabs {
ACTIVE, SCHEDULED, COMPLETE, FILTERED
}
Edit:
Here's roughly the same questions coming up in other places 1, 2, 3

How to use getActivity() without returning NULL in other functions?

How do I use getActivity or activity in other functions outside of onCreate View, when I use activity in oncreateview or other functions it returns null. in a fragment.
Is there a way to store activity and use it as a variable in a function outside of oncreateview?
There are answers for this in java but I couldn't find in Kotlin, (interchanging between them is a bit confusing)
This is onCreateView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
var mActivity = activity. // This returned Null
phRecycler = view?.findViewById(R.id.recyle)
E_shelf = view?.findViewById(R.id.E_shelf)
phoneRecycler()
return inflater.inflate(R.layout.fragment_home3, container, false)
}
this is another function in the same fragment
fun phoneRecycler() {
//All Gradients
//HSSFSheet sheet = readExcel();
val TAG = "Main"
val myMap: MutableMap<String, Int> = HashMap()
myMap["Index"] = 0
val LatestShelf = ArrayList<phonehelper>()
val exs = ArrayList<phonehelper>()
phoneRecycler!!.layoutManager = LinearLayoutManager(Activity, LinearLayoutManager.HORIZONTAL, false). // activity returns null
...
I am getting Null value for activity wherever I put it, even in the OnCreateView, also this fragment is connected to a xml
well I would suggest you to put
phRecycler = view?.findViewById(R.id.recyle)
E_shelf = view?.findViewById(R.id.E_shelf)
phoneRecycler()
inside onViewCreated() method.
Also, your answer is Simple, use
/**
*as Per Docs, for requireActivity(), "Return the FragmentActivity this fragment is currently associated with."
*/
this.requireActivity()
// or simply
requireActivity()
/**
* another method would be
* as per docs, for activity or getActivity(), "Return the FragmentActivity this fragment is currently associated with. May return null if the fragment is associated with a Context instead."
*/
val activity = this.activity
if(activity== null) throw error()
EDIT:
Please Try This Code
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home3, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
E_shelf = view.findViewById(R.id.E_shelf)
phoneRecycler()
}
fun phoneRecycler() {
//All Gradients
//HSSFSheet sheet = readExcel();
val TAG = "Main"
val myMap: MutableMap<String, Int> = HashMap()
myMap["Index"] = 0
val LatestShelf = ArrayList<phonehelper>()
val exs = ArrayList<phonehelper>()
//remove any other variable declartion
val phoneRecycler = view.findViewById(R.id.recyle)
phoneRecycler.layoutManager = LinearLayoutManager(requireActivity(), LinearLayoutManager.HORIZONTAL, false). // activity returns null
...
If a fragment is no longer attached to an activity then the activity variable is going to be null
you need to null check each time you try to use it
I guess in your situation would be nice to replace logic from OnCreateView to OnActivityCreated method. Cause in this method getActivity != null always.

I can't find fragment by its ID nor its tag

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

Get view from FragmentStatePagerAdapter or Fragment from FragmentManager

I have a problem with fragments. I am trying to get data from a fragment to an activity. I am using a FragmentStatePagerAdapter in my Activity.
The fragment is as follow, it creates some view for a user.
class FormFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val rootView = inflater.inflate(R.layout._component_main_container, container, false)
...
UICreator.create(context, rootView.entry_container) //creation of the form view ok
...
return rootView
}
//public method to get the user input
fun getUserInputData() : List<Data> {
return CreationService.getUserInput(view!!.entry_container) //same result with rootView.entry_container
}
}
Here my adapter without the arguments passing and getCount, etc..
class ViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
val fragment = FormFragment()
return fragment
}
}
And in my activity I tried these to get the current fragment and to get the user data but nothing is working.
val fragment = supportFragmentManager.findFragmentByTag("android:switcher:" + R.id.view_pager + ":" + view_pager.currentItem) as FormFragment
fragment.getUserInputData() --> NULL
val fragment = viewPagerAdapter.getItem(view_pager.currentItem) as FormFragment
fragment.getUserInputData() --> NULL
Do you know a valid solution and effective to get the current fragment and the possibility to get a reference to the linear layout view that holding the user data?

Categories

Resources