I have this code, where I want the first fragment of my FragmentStatePageAdapter(inside a viewpager), to change, depending on some boolean values.
inner class MainFragmentAdapter(fragmentManager: FragmentManager) :
FragmentStatePagerAdapter(fragmentManager) {
override fun getCount(): Int {
return MainView.Fragments.values().size
}
override fun getItem(position: Int): Fragment? {
return when (position) {
0 -> {
if (booleanOne && !booleanTwo) {
FragmentOne.newInstance()
} else if (booleanTwo && !booleanOne) {
FragmentTwo.newInstance()
} else {
FragmentThree.newInstance()
}
}
else -> null
}
}
}
As of now, the only solution I found, was when I personally knew one of the boolean values changed, I just did something similar to this :
_fragmentAdapter = MainFragmentAdapter(getLifecycleFragmentManager())
fragmentViewPager.adapter = _fragmentAdapter
So I just set the adapter once again, but it seems rather off to me to do this. Can't I force it to just calculate getItem once again?
Inside your class make these changes, notifyDataSetChanged() and then override the getItemPosition() which will be called on every notify and then just return the respective constant, POSITION_NONE if you want to see new changes or else POSITION_UNCHANGED
inner class MainFragmentAdapter(fragmentManager: FragmentManager) :
FragmentStatePagerAdapter(fragmentManager) {
override fun getCount(): Int {
return MainView.Fragments.values().size
}
override fun getItem(position: Int): Fragment? {
return when (position) {
0 -> {
if (booleanOne && !booleanTwo) {
FragmentOne.newInstance()
} else if (booleanTwo && !booleanOne) {
FragmentTwo.newInstance()
} else {
FragmentThree.newInstance()
}
notifyDataSetChanged();
}
else -> null
}
}
override fun getItemPosition(#NonNull `object`: Any): Int {
// this method will be called for every fragment in the ViewPager
return if (`object` is FragmentOne) {
POSITION_UNCHANGED // don't force a reload
} else {
// POSITION_NONE means something like: this fragment is no longer valid
// triggering the ViewPager to re-build the instance of this fragment.
POSITION_NONE
}
}
}
Related
I'm handling back button of device via fragments but while calling from nested fragments I'm facing issue. It always calls the backPressed method of MainActivity. I'm handling it as:
class MainActivity : BaseActivity() {
protected fun goToHome() {
findNavController(R.id.nav_host_fragment).navigate(R.id.global_action_pop_to_home)
}
override fun onBackPressed() {
if (supportFragmentManager.backStackEntryCount != 0) {
supportFragmentManager.popBackStackImmediate()
} else {
return super.onBackPressed()
}
}
}
Here I'm calling MainTabFragment which is having one more fragment container where I'm loading some child fragments as.
class MainTabFragment : BaseFragment() {
fun loadFragment(fragment: Fragment, addToBackStack: Boolean = false, arguments: Bundle? = null): Boolean {
//switching fragment
val fragmentTransaction: FragmentTransaction = childFragmentManager.beginTransaction()
if (addToBackStack) {
fragmentTransaction.add(R.id.main_container, fragment)
fragmentTransaction.addToBackStack(fragment.javaClass.name)
} else {
fragmentTransaction.replace(R.id.main_container, fragment)
}
fragment.arguments = arguments
fragmentTransaction.commit()
return true
}
private fun handleBack() {
val callback: OnBackPressedCallback =
object : OnBackPressedCallback(true /* enabled by default */) {
override fun handleOnBackPressed() {
view?.hideKeyboard()
}
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
}
}
This is the Child fragment what I'm adding in main_container fragment.
open class HomeFragment : BaseFragment() {
private fun handleBack() {
val callback: OnBackPressedCallback =
object : OnBackPressedCallback(true /* enabled by default */) {
override fun handleOnBackPressed() {
if (viewModel.isContact) {
showDiscardCredentialDialog()
} else {
// findNavController().popBackStack()
//Here f gets null when I press back button
val f: Fragment? = childFragmentManager.findFragmentById(R.id.main_container)
if (f is HomeFragment) // do something with f
childFragmentManager.popBackStackImmediate()
}
}
}
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
}
}
Here I want to handle back press from nested fragment and open the last added fragment. TIA.
I am trying to get a callback from one child fragment to parent fragment in kotlin and then later call a function in parent fragment to manage other layouts inside the child fragment.
I know how to get callback from a fragment to the activity and access the function in the fragment in the callback we cannot do that in Kotlin because of companion objects.
Another way is to pass context of fragment to child fragment and implement the interface using fragment's content in onAttach() but I cannot pass the context of parent fragment via constructor as I am using factory static methods.
Any help will be highly appreciated.
Child Fragment : DriverStatusFragment.kt
companion object {
#JvmStatic
fun newInstance() =
DriverStatusFragment().apply {
arguments = Bundle().apply {
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is OnCurrentOrderStatusChanged) {
orderStatusChangedCallBack = context
}
}
private fun handleUserOnlineStatus(isOnline: Boolean) {
CustomPreferences.setBooleanPreference(context!!, PrefEntity.IS_ONLINE, isOnline)
when (isOnline) {
true -> {
binding.idOnlineStatus.text = getString(R.string.online)
binding.idOnlineStatusImage.setImageResource(R.drawable.circle_green)
//testing:
orderStatusChangedCallBack?.orderStatusChanged(CONSTANTS.NEW_ORDER)
}
false -> {
binding.idOnlineStatus.text = getString(R.string.offline)
binding.idOnlineStatusImage.setImageResource(R.drawable.circle_gray)
}
}
}
Parent Fragment : HomeFragment.kt
class HomeFragment : Fragment(), OnMapReadyCallback, OnCurrentOrderStatusChanged {
companion object {
#JvmStatic
fun newInstance() =
HomeFragment().apply {
arguments = Bundle().apply {
}
}
}
fun handleOrderStatus(status: String) {
when (status) {
CONSTANTS.IDLE -> {
replaceFragment(
DriverStatusFragment.newInstance(),
DriverStatusFragment::class.java.simpleName
)
}
CONSTANTS.NEW_ORDER -> {
replaceFragment(
NewOrderFragment.newInstance(false),
NewOrderFragment::class.java.simpleName
)
}
CONSTANTS.ORDER_ACCEPTED -> {
replaceFragment(
EnRouteFragment.newInstance(CONSTANTS.ORDER_ACCEPTED),
EnRouteFragment::class.java.simpleName
)
}
CONSTANTS.ARRIVED_AT_DROP_LOCATION -> {
replaceFragment(
OrderDeliveredFragment.newInstance(CONSTANTS.ARRIVED_AT_DROP_LOCATION),
OrderDeliveredFragment::class.java.simpleName
)
}
CONSTANTS.DELIVERED -> {
replaceFragment(
DriverStatusFragment.newInstance(),
DriverStatusFragment::class.java.simpleName
)
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
this.context = context
}
**NOT RECEIVING CALLBACK HERE**
override fun orderStatusChanged(orderStatus: String) {
CommonUtils.showToast(context!!, "REAched here")
handleOrderStatus(orderStatus)
}
}
I have the following fragment handling a ViewPager2 which creates fragments (VideoFragment) on which a video is shown via ExoPlayer:
private const val IMMERSIVE_FLAG_TIMEOUT = 500L
class VideoGalleryFragment : Fragment() {
private lateinit var binding: FragmentVideoGalleryBinding
private lateinit var mediaList: MutableList<File>
private lateinit var mediaViewPager: ViewPager2
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val videoGalleryFragmentViewModel by viewModels<VideoGalleryFragmentViewModel> { viewModelFactory }
private val args by navArgs<VideoGalleryFragmentArgs>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Mark this as a retain fragment, so the lifecycle does not get restarted on config change
retainInstance = true
// Get root directory of media
//outputDirectory = getOutputDirectory(requireContext())
val rootDirectory = File(args.rootDirectory)
// Walk through all files in the root directory
// We reverse the order of the list to present the last photos first
mediaList = rootDirectory.listFiles { file ->
VIDEO_EXTENSION_WHITELIST.contains(file.extension.toUpperCase(Locale.ROOT))
}?.sortedDescending()?.toMutableList() ?: mutableListOf()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_video_gallery, container, false)
binding.apply {
lifecycleOwner = viewLifecycleOwner
viewModel = videoGalleryFragmentViewModel
}
return binding.root
}
// HERE IS MY VIEWPAGER2 SETUP
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mediaViewPager = binding.videoViewPager.apply {
offscreenPageLimit = 2
adapter = object: FragmentStateAdapter(this#VideoGalleryFragment) {
override fun getItemCount(): Int = mediaList.size
override fun createFragment(position: Int): Fragment =
VideoFragment.create(
mediaList[position]
)
}
setPageTransformer(DepthPageTransformer())
}
}
}
override fun onResume() {
super.onResume()
/*
* Before setting full screen flags, we must wait a bit to let UI settle; otherwise, we may
* be trying to set app to immersive mode before it's ready and the flags do not stick
* */
binding.container.postDelayed({
binding.container.systemUiVisibility =
FLAGS_FULLSCREEN
}, IMMERSIVE_FLAG_TIMEOUT)
}
}
And here is the Fragment class (used by the ViewPager2) showing the videos via ExoPlayer2:
class VideoFragment : Fragment() {
private var _binding: FragmentVideoBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private var player: SimpleExoPlayer? = null
private var playWhenReady: Boolean? = true
private var currentWindow: Int? = 0
private var playbackPosition : Long? = 0
private var resource: String? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentVideoBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val args = arguments ?: return
args.getString(VideoFragment.VIDEO_FILE_NAME_KEY)?.let {
resource = it
}
}
override fun onDestroyView() {
super.onDestroyView()
releasePlayer()
_binding = null
}
override fun onStart() {
super.onStart()
if(Util.SDK_INT >= 24){
initializePlayer(resource)
}
}
override fun onResume() {
super.onResume()
if((Util.SDK_INT < 24 || player == null)){
initializePlayer(resource)
}
}
private fun initializePlayer(videoFilePath: String?){
player = ExoPlayerFactory.newSimpleInstance(activity)
binding.videoViewGallery.player = player
videoFilePath?.let {
val mediaSource: MediaSource =
ProgressiveMediaSource.Factory(DefaultDataSourceFactory(activity, "exoplayer-mylim"))
.createMediaSource(Uri.parse(it))
playWhenReady?.let { playWhenReady ->
(player as SimpleExoPlayer).playWhenReady = playWhenReady
}
currentWindow?.let { currentWindow ->
playbackPosition?.let {playbackPosition ->
(player as SimpleExoPlayer).seekTo(currentWindow, playbackPosition)
}
}
(player as SimpleExoPlayer).prepare(mediaSource, false, false)
}
}
private fun releasePlayer(){
player?.stop()
player?.release()
player = null
}
override fun onPause() {
super.onPause()
if(Util.SDK_INT < 24){
releasePlayer()
}
}
override fun onStop() {
super.onStop()
if(Util.SDK_INT >= 24){
releasePlayer()
}
}
companion object {
private const val VIDEO_FILE_NAME_KEY = "video_file_name"
fun create(video: File) = VideoFragment().apply {
arguments = Bundle().apply {
putString(VIDEO_FILE_NAME_KEY, video.absolutePath)
}
}
}
}
My problem is that when I switch between the VideoFragments by swiping, the previous Exoplayer is not released properly so that I can still hear the audio of the first video after starting a 2nd video.
Although I have put a releasePlayer() method for releasing the player into the appropriate lifecycle methods, it seems to me that ViewPager does not care about that.
How can I stop/release an ExoPlayer instance when used with ViewPager?
With ViewPager2, your Fragment can be pre-loaded before it is actually displayed to the user. One way to solve your problem is to listen for page changes and releasePlayer() from there, rather than waiting for the Fragment to be destroyed. See the documentation for ViewPager2.OnPageChangeCallback here
I know it is late But Might help someone, I also ran into the same issue. The solution lies in using StatePagerAdapter.
class ViewPagerAdapter extends FragmentStatePagerAdapter {
public ViewPagerAdapter(FragmentManager manager) {
super(manager, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
#Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}
#Override
public int getCount() {
return mFragmentList.size();
}
public void addFragment(Fragment fragment) {
mFragmentList.add(fragment);
}
#Override
public CharSequence getPageTitle(int position) {
return mFragmentTitleList.get(position);
}
}
Old That I was Using
class ViewPagerAdapter extends FragmentPagerAdapter {
public ViewPagerAdapter(FragmentManager manager) {
super(manager);
}
#Override
public Fragment getItem(int position) {
return mFragmentList.get(position);
}
#Override
public int getCount() {
return mFragmentList.size();
}
public void addFragment(Fragment fragment) {
mFragmentList.add(fragment);
}
#Override
public CharSequence getPageTitle(int position) {
return mFragmentTitleList.get(position);
}
}
The Difference is on Parent Classes
FragmentStatePagerAdapter <== use this Now
class ViewPagerAdapter extends FragmentStatePagerAdapter {
public ViewPagerAdapter(FragmentManager manager) {
super(manager, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
}
FragmentPagerAdapter <== I was using
class ViewPagerAdapter extends FragmentPagerAdapter {
public ViewPagerAdapter(FragmentManager manager) {
super(manager);
}
FragmentPagerAdapter this provides a method which is now deprecated can be seen below:
#Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser){
if (!loaded){
loadStatus();
}
}
}
this was used to manage resources/play/pause based on if a fragment is visible to the user in the View Pager. Deprecated
Now If you want to handle resources/state of the fragment you should use State Pager Adapter as I have described above. Implementing StatePagerAdapter calls onPause() and onResume() methods of a fragment Based on Its Visibility to User.
So answer to the question is Implement Adapter Like StatePagerAdapter and pause the player on Fragment's onPause() method and Resume it in the onResume() method.
I'm trying to handle fragment clicks in FragmentPagerAdapter, but sometimes I'm getting Fatal Exception: kotlin.UninitializedPropertyAccessException, which says that click listener property is not initialized.
So here is code for the PagerAdapter
class ApplicationListPagerAdapter(
fm: FragmentManager,
private val onListItemClick: (isSent: Boolean, application: Application) -> Unit,
private val onGoToScholarshipsTabClicked: () -> Unit,
private val onGoToPicksTabClicked: () -> Unit
): FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT),
ApplicationListFragment.ClickListener {
override fun getCount(): Int {
return 2
}
override fun getItem(position: Int): Fragment {
val isSent = position == 1
val fragment = ApplicationListFragment.newInstance(isSent)
fragment.setApplicationSelectListener(this)
return fragment
}
override fun getPageTitle(position: Int): CharSequence? {
return if (position == 0) "PICKS" else "SENT"
}
override fun applicationSelected(isSent: Boolean, application: Application) {
onListItemClick(isSent, application)
}
override fun goToScholarshipsTabClicked() {
onGoToScholarshipsTabClicked()
}
override fun goToPicksTabClicked() {
onGoToPicksTabClicked()
}
}
Code how I initialize it in fragment
private lateinit var clickListener: ClickListener
fun setApplicationSelectListener(clickListener: ClickListener) {
this.clickListener = clickListener
}
interface ClickListener {
fun applicationSelected(isSent: Boolean, application: Application)
fun goToScholarshipsTabClicked()
fun goToPicksTabClicked()
}
And here in onClick callback I'm getting crashes sometimes for some users.
private fun initRecyclerView(applications: List<Application>) {
application_list_recyclerView.adapter = ApplicationListItemsAdapter(
context!!,
applications.toMutableList(),
isSent,
this,
applicationViewModel.applicationService,
onClick = {
clickListener.applicationSelected(isSent, it)
},
onDelete = { application: Application, count: Int ->
scholarshipViewModel.unFavoriteScholarship(application.scholarship)
sharedViewModel.deletePickedScholarship(application.scholarship)
applicationSharedViewModel.updatePicksCount(count)
if (count == 0) {
showNoApplicationsFragment()
}
},
onUndoDelete = { application: Application, count: Int ->
sharedViewModel.undoDeletedScholarship(application.scholarship)
applicationSharedViewModel.updatePicksCount(count)
if (count == 1) {
hideNoApplicationsFragment()
}
}
)
application_list_recyclerView.layoutManager = LinearLayoutManager(context!!)
}
Thanks for any advice and I hope my explanation makes senes for everybody
Interface
interface OnItemClickListener {
//just a random method
fun itemClicked(positon: Int)
}
Inside Your Adapter class
private lateinit var listener: OnItemClickListener
fun attachListener(listener: OnItemClickListener){
this.listener = listener
}
Acticity/ Fragment
class MainActivity(): AppCompatActivity(), OnItemClickListener{
// implement interface in case you wanna use methods...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_chat)
// calling the listener method
myAdapter.attachListener(this)
}
}
above myAdapter is your adapter must be init first which is associate to your adapter class
class ScheduleUpdateDialog(private val schedules: ArrayList<Schedule>)
: DialogFragment() {
...
inner class DialogListener : DialogInterface.OnClickListener {
override fun onClick(dialog: DialogInterface?, which: Int) {
when(which) {
DialogInterface.BUTTON_POSITIVE -> {
val content = calendarScheduleText.text.toString()
if(content != "") {
db.collection("schedules").document(id!!)
.update("content", content)
schedule.content = content
mainActivity.onDialogClick()
}
}
DialogInterface.BUTTON_NEUTRAL -> {
db.collection("schedules").document(id!!)
.delete()
schedules.removeAt(index!!)
mainActivity.onDialogClick()
}
}
}
}
}
You can check return value for the element that has been removed.
abstract fun removeAt(index: Int): E
Removes an element at the specified index from the list.
Return the element that has been removed.