Getting view Null in fragment when onPageSelected() of ViewPager called Kotlin - android

Using Kotlin Android Extensions for views initialization
Activity Code for PageSelected:
introPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}
override fun onPageSelected(position: Int) {
when(position) {
0 -> (pagerAdapter.getItem(position) as SearchFragment).startViewAnimations()
}
}
override fun onPageScrollStateChanged(state: Int) {
}
})
Fragment Method:
fun startViewAnimations() {
rippleBack?.startRippleAnimation()
centerImage?.startAnimation(AnimationUtils.loadAnimation(activity, R.anim.rotate_indefinitely))
val handler = Handler()
handler.postDelayed({
relAllViews?.visibility = View.VISIBLE
relAllViews?.animate()?.alpha(1.0f)
rippleBack?.animate()?.alpha(0.0f)
rippleBack?.visibility = View.GONE
rippleBack?.stopRippleAnimation()
centerImage?.clearAnimation()
}, 3500)
}
If I use the same function in the Fragment's onViewCreated() it works fine code sample:
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
startViewAnimations()
}
Full Fragment Code:
package com.beeland.consumer.fragment.appintro
import android.os.Bundle
import android.os.Handler
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnimationUtils
import com.beeland.consumer.R
import com.beeland.consumer.utils.Utils
import kotlinx.android.synthetic.main.fragment_search.*
/**
* #author Yash
* #since 23-04-2018.
*/
class SearchFragment : Fragment() {
/**
* Method to replace layouts in App Intro Section
*
* #return IntroFragment's new instance
*/
fun newInstance(): SearchFragment {
val intro = SearchFragment()
val args = Bundle()
args.putString("", "")
intro.arguments = args
return intro
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater!!.inflate(R.layout.fragment_search, container, false)
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
startViewAnimations()
}
fun startViewAnimations() {
rippleBack?.startRippleAnimation()
centerImage?.startAnimation(AnimationUtils.loadAnimation(activity, R.anim.rotate_indefinitely))
val handler = Handler()
handler.postDelayed({
relAllViews?.visibility = View.VISIBLE
relAllViews?.animate()?.alpha(1.0f)
rippleBack?.animate()?.alpha(0.0f)
rippleBack?.visibility = View.GONE
rippleBack?.stopRippleAnimation()
centerImage?.clearAnimation()
handler.postDelayed({
Utils.bounceAnimationVisible(txt5Km)
}, 300)
handler.postDelayed({
Utils.bounceAnimationVisible(txtPostalDelivery)
}, 600)
handler.postDelayed({
Utils.bounceAnimationVisible(img1)
}, 900)
handler.postDelayed({
Utils.bounceAnimationVisible(img2)
}, 1200)
handler.postDelayed({
Utils.bounceAnimationVisible(img3)
}, 1500)
handler.postDelayed({
Utils.bounceAnimationVisible(imgRes1)
Utils.bounceAnimationVisible(imgRes2)
Utils.bounceAnimationVisible(imgRes3)
Utils.bounceAnimationVisible(imgRes4)
}, 1800)
}, 3500)
}
}

View pager by default caches only 3 fragments. Current, previous and next.
When you move to some page all pages which are out of this range call onDestroyView() and the view is destroyed i.e. it becomes null. When this page gets into the range again it must create the view again thus calling onCreateView(). The view is null while the it is creating
Range in which fragments (actually views) will are cached can be customized with setOffscreenPageLimit(int limit).
Hope this solves your problem.

Related

Viewpager2, tablayout doesn't work with lazyloader

I have ViewPager2, Tablayout with Lazyloder.
At the first getting in the fragment it works, dots move while loading but after I swipe from one fragment to another and back to my first fragment dots don't move anymore.
.xml
<com.agrawalsuneet.dotsloader.loaders.LazyLoader
android:id="#+id/dots_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="20dp"
android:layout_marginTop="30dp"
app:lazyloader_animDur="400"
app:lazyloader_dotsDist="10dp"
app:lazyloader_dotsRadius="7dp"
app:lazyloader_firstDelayDur="150"
app:lazyloader_firstDotColor="#fff"
app:lazyloader_interpolator="#android:anim/decelerate_interpolator"
app:lazyloader_secondDelayDur="300"
app:lazyloader_secondDotColor="#fff"
app:lazyloader_thirdDotColor="#fff"
tools:ignore="MissingClass" />
fragment
#AndroidEntryPoint
class InspirationQuotesFragment :
BaseFragment<FragmentInspirationQuotesBinding, InspirationQuotesViewModel>
(FragmentInspirationQuotesBinding::inflate) {
override val vm: InspirationQuotesViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
backgroundAnimation()
getRandomPicture()
onFloatingActionClick()
}
override fun onResume() {
super.onResume()
}
private fun getRandomPicture() {
layout.floatingActionBtn.setOnClickListener {
onFloatingActionClick()
}
}
private fun onFloatingActionClick() {
layout.floatingActionBtn.animate().apply {
rotationBy(360f)
duration = 1000
}.start()
layout.ivRandomPicture.visibility = View.GONE
makeApiRequest()
}
private fun backgroundAnimation() {
val animationDrawable: AnimationDrawable = layout.rlLayout.background as AnimationDrawable
animationDrawable.apply {
setEnterFadeDuration(1000)
setExitFadeDuration(3600)
start()
}
}
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
if (response.fileSizeBytes < 600000) {
Log.d("fragment", "itGetsValue")
Glide.with(requireContext()).load(response.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
onFloatingActionClick()
}
}
}
}
viewpagerfragment
package com.example.learningmanager.fragments
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.View
import androidx.fragment.app.viewModels
import com.example.learningmanager.base.ui.BaseFragment
import com.example.learningmanager.databinding.FragmentViewPagerBinding
import com.example.learningmanager.fragments.inspirationquotes.ui.InspirationQuotesFragment
import com.example.learningmanager.fragments.notesmanager.ui.NotesManagerFragment
import com.example.learningmanager.fragments.setgoals.ui.SetGoalsFragment
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
#AndroidEntryPoint
class ViewPagerFragment #Inject constructor() : BaseFragment<FragmentViewPagerBinding, ViewPagerViewModel>(
FragmentViewPagerBinding::inflate
) {
override val vm: ViewPagerViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// viewpager with list of fragments
val fragmentList = arrayListOf<Fragment>(
InspirationQuotesFragment(),
NotesManagerFragment(),
SetGoalsFragment()
)
val adapter = ViewPagerAdapter(
fragmentList,
childFragmentManager,
lifecycle
)
layout.viewPager.adapter = adapter
// initialize tablayout with names
TabLayoutMediator(layout.tabLayout, layout.viewPager) {tab, position ->
when (position) {
0 -> tab.text = "Inspiration"
1 -> tab.text = "Notes"
2 -> tab.text = "Set Goals"
}
}.attach()
listener if tab changes and it is possible to make specific action
layout.tabLayout.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
}
}
viewpageradapter
class ViewPagerAdapter(list: ArrayList<Fragment>, fm: FragmentManager, lifecycle: Lifecycle) :
FragmentStateAdapter(fm, lifecycle) {
val fragmentList = list
override fun getItemCount(): Int {
return fragmentList.size
}
override fun createFragment(position: Int): Fragment {
return fragmentList[position]
}
}
Have you got an information how to fix it or maybe change to another library?
Thank you
I ran the same sample at my end. I found out that the trick is to use initView() on onResume of a fragment. Which internally calls startLoading.
Sample code:
lateinit var dotsLoader: LazyLoader
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_test1, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dotsLoader = view.findViewById(R.id.dots_loading)
}
override fun onResume() {
super.onResume()
dotsLoader.initView()
}

How to add/remove tabs in ViewPager2 at runtime? Using only one fragment

In the app I'm testing this concept, the app starts with one tab and the user can click a button inside the fragment and add a new tab with the name of an item inside a pre-defined list of movie titles. Imagine the list has 100 titles, how can I re-use the same fragment for each tab created so I don't have to make 100 fragments (one for each title)?
This link shows how to add/remove tabs using several fragments and a sample app.
class DynamicViewPagerAdapter(
fragmentActivity: FragmentActivity,
private val titleId: Int
) : FragmentStateAdapter(fragmentActivity) {
override fun createFragment(position: Int): Fragment {
return DynamicFragment.getInstance(titleId)
}
override fun getItemCount(): Int {
return titles.size
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun containsItem(itemId: Long): Boolean {
return titles.contains(titles[itemId.toInt()])
}
fun addTab(title: String) {
titles.add(title)
notifyDataSetChanged()
}
fun addTab(index: Int, title: String) {
titles.add(index, title)
notifyDataSetChanged()
}
fun removeTab(name: String) {
titles.remove(name)
notifyDataSetChanged()
}
fun removeTab(index: Int) {
titles.removeAt(index)
notifyDataSetChanged()
}
}
You could achieve this by using one Fragment that you pass the id of the item to. The Fragment will then need to retrieve the Item from the List and display the content of that item. Here is an example pager adapter.
private inner class SamplePagerAdapter(activity: AppCompatActivity, private val itemId: Int): FragmentStateAdapter(activity) {
override fun getItemCount(): Int {
return 100
}
override fun createFragment(position: Int): Fragment {
return ItemFragment.newInstance(itemId)
}
}
I figured it out, a very important part was to keep ordinals. In the process of removing tabs, the adapter makes multiple calls to getItemId() and containsItem(). In this process the adapter uses the position of the tab and the ordinal of the item in the tab. In the example I found which uses different fragments and a predefined number of them, they use enum to get the ordinals, while I used a MutableMap (titles and keys; ordinals as values). Here's the link to the finished test app.
I used these global variable (only as a test):
// pre-defined list of titles
val testMovieTitles = listOf(
"Hulk", "Chucky", "Cruella", "Nobody", "Scar Face",
"Avatar", "Joker", "Profile", "Saw", "Ladies")
val titles = mutableListOf("All Movies")
val titlesOrdinals: MutableMap<String, Int> = mutableMapOf("All Movies" to 0)
const val MY_LOG = "MY_LOG"
The activity:
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.example.testmyviewpager2.*
import com.example.testmyviewpager2.databinding.ActivityDynamicViewPagerBinding
import com.google.android.material.tabs.TabLayoutMediator
class DynamicViewPagerActivity : AppCompatActivity() {
private var binding: ActivityDynamicViewPagerBinding? = null
var titleIncrementer = 0 // to use the next tile until it doesn't match one of the tabs
val activityViewPagerAdapter: DynamicViewPagerAdapter by lazy {
DynamicViewPagerAdapter(this)}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDynamicViewPagerBinding.inflate(layoutInflater)
setContentView(binding!!.root)
setUpTabs()
addTabFabOnClick()
}
private fun setUpTabs() {
binding!!.dynamicViewPager.offscreenPageLimit = 4
binding!!.dynamicViewPager.adapter = activityViewPagerAdapter
// Set the title of the tabs
TabLayoutMediator(binding!!.dynamicTabLayout, binding!!.dynamicViewPager) { tab, position ->
tab.text = titles[position]
}.attach()
}
private fun addTabFabOnClick() {
binding!!.addTabFab.setOnClickListener {
val nextTitlePosition = titles.size - 1
val nextOrdinalId = titlesOrdinals.size - 1
var nextTitle = testMovieTitles[nextTitlePosition]
// if a title has been added before, don't add it
// new tabs cannot have the same name as old tabs
while(titles.contains(nextTitle)) {
titleIncrementer++
nextTitle = testMovieTitles[nextTitlePosition + titleIncrementer]
}
if (titleIncrementer > 0) { Log.d("${MY_LOG}Activity", "incrementer: $titleIncrementer") }
if(!titles.contains(nextTitle)) {
activityViewPagerAdapter.addTab(nextOrdinalId+1, nextTitle)
} else {
Log.d("${MY_LOG}Activity", "\t\t titles contains next title \t\t $titles $nextTitle")}
}
}
}
The re-usable fragment:
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.testmyviewpager2.*
import kotlinx.android.synthetic.main.fragment_dynamic.*
class DynamicFragment : Fragment() {
private var fragmentViewPagerAdapter: DynamicViewPagerAdapter? = null
private var titleToDisplay = "All Movies"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_dynamic, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// get the adapter instance from the main activity
fragmentViewPagerAdapter = (activity as? DynamicViewPagerActivity)!!.activityViewPagerAdapter
removeButtonOnClick()
dynamic_fragment_text.text = titleToDisplay
Log.d("${MY_LOG}fragCreated", "name: ${titleToDisplay}")
super.onViewCreated(view, savedInstanceState)
}
override fun onDestroy() {
Log.d("${MY_LOG}destroyed", "\t\t\t $titles")
Log.d("${MY_LOG}destroyed", "\t\t\t $titlesOrdinals")
super.onDestroy()
}
private fun removeButtonOnClick() {
removeButton.setOnClickListener {
val numOfTabs = titles.size
if (numOfTabs > 1 && titleToDisplay != "All Movies") {
fragmentViewPagerAdapter!!.removeTab(titleToDisplay)
}
}
}
fun setTitleText(title: String) {
titleToDisplay = title
}
companion object{
//The Fragment retrieves the Item from the List and display the content of that item.
fun getInstance(titleId: Int): DynamicFragment {
val thisDynamicFragment = DynamicFragment()
val titleToDisplay = titles[titleId]
thisDynamicFragment.setTitleText(titleToDisplay)
return thisDynamicFragment
}
}
}
The ViewPager2 Adapter:
import android.util.Log
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.example.testmyviewpager2.*
class DynamicViewPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
private val theActivity = DynamicViewPagerActivity()
override fun createFragment(position: Int): Fragment {
// Used this to change the text inside each fragment
return DynamicFragment.getInstance(titles.size-1)
}
override fun getItemCount(): Int {
return titles.size
}
override fun getItemId(position: Int): Long {
return titlesOrdinals[titles[position]]!!.toLong()
}
// called when a tab is removed
override fun containsItem(itemId: Long): Boolean {
var thisTitle = "No Title"
titlesOrdinals.forEach{ (k, v) ->
if(v == itemId.toInt()) {
thisTitle = k
}
}
return titles.contains(thisTitle)
}
fun addTab(ordinal: Int, title: String) {
titles.add(title)
// don't rewrite an ordinal
if(!titlesOrdinals.containsKey(title)) {
titlesOrdinals[title] = ordinal
}
notifyDataSetChanged()
Log.d("${MY_LOG}created", "\t\t\t $titles")
Log.d("${MY_LOG}created", "\t\t\t $titlesOrdinals")
}
fun removeTab(name: String) {
titles.remove(name)
notifyDataSetChanged()
Log.d("${MY_LOG}removeTab", "----------------")
}
fun removeTab(index: Int) {
titles.removeAt(index)
notifyDataSetChanged()
}
}

How to access object in viewModel from outside activity or fragment class in Kotlin?

I have a app in which I'm trying go implement the MVVM pattern for the first time.
I have a simple fragment with the corresponding viewModel and a seperate class that deals with the swipeToDelete from the recyclerView which is in the fragment.
The viewModel looks like this:
import androidx.lifecycle.ViewModel
class ListViewModel : ViewModel() {
var keedList: ArrayList<Keed> = ArrayList()
}
And the SwipeToDelete class like this:
import android.util.Log
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
class SwipeToDelete : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
// since the feature is not used, simply return "false"
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target:
RecyclerView.ViewHolder) = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position: Int = viewHolder.adapterPosition
keedList.removeAt(position) // this is not working, "keedList" is red.
viewModel.keedList.removeAt(position) // this is not working either... "viewModel" is red.
myAdapter.notifyItemRemoved(position)
}
}
the other files should be irrelevant I guess.
Now the problem ist, that I can't remove my swiped off item in the "onSwiped" function since it won't recognize my "keedList" or the "viewModel" in the function because the SwipeToDelete class is neighter fragment or activity (I tried both cases...).
Is there a general flaw on how I'm designing it? How could that be solved?
Thanks for your help.
And here is my code in the fragment before using on of the two approaches:
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_list.*
import marcelfuchs.example.org.keed.databinding.FragmentListBinding
lateinit var myAdapter: RecyclerAdapter
class MainListFragment : Fragment() {
private val viewModel: ListViewModel by activityViewModels()
private var _binding: FragmentListBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DataBindingUtil.inflate(inflater, R.layout.fragment_list, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
myAdapter = RecyclerAdapter(viewModel.keedList)
rv_killsDeaths.layoutManager = LinearLayoutManager(MainActivity())
rv_killsDeaths.adapter = myAdapter
binding.fab.setOnClickListener {
findNavController().navigate(R.id.action_listFragment_to_enterItemsFragment)
}
// close the softKeyboard as it keeps on opening when returning from NewItemFragment
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(requireView().windowToken, 0)
val itemTouchHelper = ItemTouchHelper(SwipeToDelete())
itemTouchHelper.attachToRecyclerView(rv_killsDeaths)
super.onViewCreated(view, savedInstanceState)
}
//Fragments outlive their views. Make sure you clean up any references to the binding class instance in the fragment's onDestroyView() method.
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
You could pass a listener to the SwipeToDelete class and invoke it whenever an item in the RecyclerView is swiped.
class SwipeToDelete(private val deletionListener: (Int) -> Unit) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position: Int = viewHolder.adapterPosition
deletionListener.invoke(position)
}
}
In your fragment, you can remove that item from your list
val swipeToDeleteCallback = SwipeToDelete { position ->
viewModel.keedList.removeAt(position)
myAdapter.notifyItemRemoved(position)
}
val itemTouchHelper = ItemTouchHelper(swipeToDeleteCallback)
itemTouchHelper.attachToRecyclerView(rv_killsDeaths)
Or, you could create an object within the fragment instead of a separate class (Makes sense if it is not being reused in other fragments/activities). Like so:
val swipeToDeleteCallback = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position: Int = viewHolder.adapterPosition
viewModel.keedList.removeAt(position)
myAdapter.notifyItemRemoved(position)
}
}
val itemTouchHelper = ItemTouchHelper(swipeToDeleteCallback)
itemTouchHelper.attachToRecyclerView(rv_killsDeaths)

Android Studio Kotlin - Disable user gestures in BottomSheetDialogFragment

I need to disable user from dragging bottom sheet, but I have no idea on how to do that.
This is my fragment:
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
class MyFragment : BottomSheetDialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val main = inflater.inflate(R.layout.fragment_layout, container, false)
return main
}
companion object {
fun newInstance(): MyFragment = newInstance()
}
}
This is how I show it:
val test = MyFragment()
test.show(supportFragmentManager, "test")
You need to override onCreateDialog() and put below code
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
dialog.setOnShowListener {
val bottomSheet = (it as BottomSheetDialog).findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) as FrameLayout?
val behavior = BottomSheetBehavior.from(bottomSheet!!)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_DRAGGING) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
})
}
// Do something with your dialog like setContentView() or whatever
return dialog
}
or check this link
How to disable BottomSheetDialogFragment dragging

Null Pointer within Fragment using Synthetic

I am getting null pointers (sometimes) on views within fragments using synthetic.
I do not know what is wrong because I am declaring the fragments in XML and I think that when I call the populate method from the Activity, the root view is already created
Anyway, I do not believe is correct check this each time I call the populate...
I write an example code of what happens (the null pointer would be on tv):
The fragment:
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import kotlinx.android.synthetic.main.fragment_foo.*
class FooFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_foo, container, false)
}
fun populate(fooName: String) {
tv.text = fooName
}
}
And the XML related within the XML of the Activity related
<fragment
android:id="#+id/fFoo"
android:name="com.foo.FooFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout="#layout/fragment_foo"/>
This would the Activity related:
class FooActivity : AppCompatActivity() {
private lateinit var fooFragment: FooFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_foo)
fooFragment = fFoo as FooFragment
}
private fun loadDataProductionInfo(productionId: Long) {
FooAPIParser().getFoo {
initFoo(it.fooName)
}
}
private fun initFoo(fooName: String) {
fooFragment.populate(fooName)
}
}
And finally the XML of the specific fragment:
<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="wrap_content">
<TextView
android:id="#+id/tvFoo"
style="#style/Tv"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Kotlin synthetic properties are not magic and work in a very simple way. When you access btn_K, it calls for getView().findViewById(R.id.tv)
The problem is that you are accessing it too soon getView() returns null in onCreateView. Try doing it in the onViewCreated method:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//Here
}
}
Using views outside onViewCreated.
tv?.text = "kotlin safe call"
If you are going to use Fragments you need to extend FragmentActivity
Thinking in your answers I have implemented a patch. I do not like very much, but I think it should work better than now. What do you think?
I would extend each Fragment I want to check this from this class:
import android.os.Bundle
import android.os.Handler
import android.view.View
import androidx.fragment.app.Fragment
open class BaseFooFragment : Fragment() {
private var viewCreated = false
private var attempt = 0
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewCreated = true
}
fun checkViewCreated(success: () -> Unit) {
if (viewCreated) {
success()
} else {
initLoop(success)
}
}
private fun initLoop(success: () -> Unit) {
attempt++
Handler().postDelayed({
if (viewCreated) {
success()
} else {
if (attempt > 3) {
return#postDelayed
}
checkViewCreated(success)
}
}, 500)
}
}
The call within the Fragment would be more or less clean:
fun populate(fooName: String) {
checkViewCreated {
tv.text = fooName
}
}
Finally, I have received help to find out a better answer.
open class BaseFooFragment : Fragment() {
private var listener: VoidListener? = null
private var viewCreated = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewCreated = true
listener?.let {
it.invoke()
listener = null
}
}
override fun onDestroyView() {
viewCreated = false
super.onDestroyView()
}
fun doWhenViewCreated(success: VoidListener) {
if (viewCreated) {
success()
} else {
listener = success
}
}
}
The VoidListener is simply this:
typealias VoidListener = () -> Unit
One way to do this more generic (for example, when you want to use more than one listener) could be like this:
open class BaseFooFragment : Fragment() {
private val listeners: MutableList<VoidListener> = mutableListOf()
private var viewCreated = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewCreated = true
listeners.forEach { it() }
listeners.clear()
}
override fun onDestroyView() {
viewCreated = false
super.onDestroyView()
}
fun doWhenViewCreated(success: VoidListener) {
if (viewCreated) {
success()
} else {
listeners.add(success)
}
}
}

Categories

Resources