I want an alert dialog to be shown, when an user scrolls from one page to the other inside a ViewPager2. I know about OnPageChangeCallback interface, but cannot figure out how to use it.
class showAlert() : Fragment(){
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
binding?.ViewPager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback(){
override fun onPageScrollStateChanged(state: Int) {
createAlert()
}
})
}
...
private fun createAlert() : Dialog {
return AlertDialog.Builder(childFragmentManager)
.setTitle("Exit?")
.setMessage("If you leave, then the data will be lost")
.setNegativeButton(android.R.string.VideoView_error_text_unknown, new onClickListener())
}
...
}
Again, the whole point is, when the user scrolls from one fragment to another inside the viewpager, an alert dialog should be shown, asking it whether it wants to move to another fragment or not.
Related
I open a new bottomSheet from a bottomSheet. When service result is successful I call dismiss() and open new fragment. This works most of times but sometimes previous bottomSheet now dismissing. For example when an alertDialog is shown and I close bottom sheet then reopen it and this issue is occurring.
First BottomSheet:
btnSend.click {
viewModel.callServiceFunction()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
observe(viewModel.serviceSuccessLiveData) {
dismiss()
navigator.navigateToSecondSheet(parentFragmentManager)
}
}
navigateToSecondSheet(fm: FragmentManager) {
SecondSheet.show(fm)
}
Second Bottom Sheet:
companion object {
fun show(fm: FragmentManager) {
SecondSheet().show(fm, "TagA")
}
}
So how can I get the first bottom sheet to always be dismissed?
maybe use:
dismissAllowingStateLoss()
edit: 2020.4.12 correct typo from Button.performClick() to button.performClick()
I am writing an app which should display a splash page/fragment for a
few seconds at start then display the next fragment in the navgraph. There are seven fragments in the navgraph which I can navigate around those fragments just fine.
The issue is with the splash fragment, I can only get the splash fragment to display/inflate when the button.onClickListener is set to accept a manual user
input -> click. (vs using button.performClick())
The desired end result is to display a fragment layout consisting of an image view and a text view for a few seconds at app start before displaying the next fragment layout in the navgraph, without having the user to click or press anything.
I have tried using threadsleep, a runnable with a handler, and even a while loop with performClick(). None of which have yielded acceptable results. The closest I have come to getting the desired result is the following:
class SplashFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater,
rootContainer: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflater(R.layout.fragment_splash, rootContainer, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button.setOnClickListener {
updateABode()
}
// pizzaLoop initialized to give about 4 seconds delay
val pizzaLoop = 1500000000
while (pizzaLoop > 0) {
pizzaLoop--
if (pizzaLoop == 0) {
button.performClick()
}
}
private fun updateABode {
val ABode = "A" // hard coded for testing purposes
when (ABode) {
"B" -> // for testing purposes only -- does nothing
"A" -> findNavController().navigate(R.id.action_splashFragment_to_firmwareFragment)
}
}
}
With the pizzaLoop installed, the splash fragment will not inflate, but I do see the delay via the firmware screen update. (intially all I get is a white blank screen then subsequent calls to the SplashFragment class show nothing but the firmwareFragment screen (next in the navgraph) -- and the pizzaLoop delay is noticable).
When I comment out the pizzaLoop then the splash fragment displays as intended but I have to click the button to bring up the next fragment in the navgraph (the rest of the navgraph works fine).
It's like the button.performClick() method is preventing the inflation of the splash fragment.
EDIT: 2020.4.12 TO PROPERLY POST SOLUTION.
class SplashFragment : Fragment() {
private val handler: Handler = Handler()
private val updateRunnable: Runnable = Runnable { updateABode() }
override fun onCreateView(inflater: LayoutInflater,
rootContainer: ViewGroup?,
savedInstanceState: Bundle?): View {
return inflater.inflater(R.layout.fragment_splash, rootContainer, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button.setOnClickListener {
handler.removeCallbacks(updateRunnable)
}
handler.postDelayed(updateRunnable, 4000)
}
private fun updateABode {
val ABode = "A" // hard coded for testing purposes
when (ABode) {
"B" -> // for testing purposes only -- does nothing
"A" -> findNavController().navigate(R.id.action_splashFragment_to_firmwareFragment)
}
}
}
I have a Fragment that is a RecyclerView, its ViewModel that does a Room operation - add(). If the database is empty, that Fragment should show an AlertDialog that allows the user to either dismiss or create a new entry.
CrimeListFragment and relevant bits:
class CrimeListFragment:
Fragment(),
EmptyAlertFragment.Callbacks {
interface Callbacks {
fun onCrimeClicked(crimeId: UUID)
}
//==========
private var callback: Callbacks? = null
private lateinit var crimeRecyclerView: RecyclerView
private val crimeListViewModel: CrimeListViewModel by lazy {
ViewModelProviders.of(this).get(CrimeListViewModel::class.java)
}
//==========
override fun onAttach(context: Context) {
super.onAttach(context)
callback = context as Callbacks?
}
override fun onCreate(savedInstanceState: Bundle?) {}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
crimeListViewModel.crimeListLiveData.observe( //crimeListLiveData: LiveData<List<Crime>>
viewLifecycleOwner,
Observer { crimes ->
crimes?.let {
Log.i(TAG, "Retrieved ${crimes.size} crimes.")
updateUI(crimes)
}
}
)
}
override fun onDetach() {
super.onDetach()
callback = null
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {}
override fun onOptionsItemSelected(item: MenuItem): Boolean {}
override fun onCreateSelected() = createNewCrime()
//==========
private fun updateUI(crimes: List<Crime>) {
if(crimes.isEmpty()) {
Log.d(TAG, "empty crime list, show empty dialog")
showEmptyDialog()
}
(crimeRecyclerView.adapter as CrimeListAdapter).submitList(crimes)
Log.d(TAG, "list submitted")
}
private fun showEmptyDialog() {
Log.d(TAG, "show empty dialog")
EmptyAlertFragment.newInstance().apply {
setTargetFragment(this#CrimeListFragment, REQUEST_EMPTY)
show(this#CrimeListFragment.requireFragmentManager(), DIALOG_EMPTY)
}
}
private fun createNewCrime() {
val crime = Crime()
crimeListViewModel.addCrime(crime)
callback?.onCrimeClicked(crime.id)
Log.d(TAG, "new crime added")
}
//==========
companion object {}
//==========
private inner class CrimeHolder(view: View)
: RecyclerView.ViewHolder(view), View.OnClickListener {}
private inner class CrimeListAdapter
: ListAdapter<Crime, CrimeHolder>(DiffCallback()) {}
private inner class DiffCallback: DiffUtil.ItemCallback<Crime>() {}
}
My EmptyAlertFragment:
class EmptyAlertFragment: DialogFragment() {
interface Callbacks {
fun onCreateSelected()
}
//==========
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(activity!!)
builder.setPositiveButton("Create") {
_, _ ->
targetFragment?.let { fragment ->
(fragment as Callbacks).onCreateSelected()
}
}
builder.setNegativeButton("Cancel") {
dialog, _ ->
dialog.dismiss()
}
val alert = builder.create()
alert.apply {
setTitle("Crime list empty!")
setMessage("Do you want to create a new crime?")
}
return alert
}
//==========
companion object {
fun newInstance(): EmptyAlertFragment {
return EmptyAlertFragment()
}
}
}
And finally my MainActivity:
class MainActivity:
AppCompatActivity(),
CrimeListFragment.Callbacks {
override fun onCreate(savedInstanceState: Bundle?) {}
//==========
override fun onCrimeClicked(crimeId: UUID) {
val crimeFragment = CrimeDetailFragment.newInstance(crimeId)
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, crimeFragment)
.addToBackStack("crime")
.commit()
}
}
Basically the flow is this:
App launched, CrimeListFragment observes database, updateUI() gets called, database is empty so alert pops up aka EmptyAlertFragment gets shown, click on Create -> onCreateSelected() callback to CrimeListFragment.
onCreateSelected() calls createNewCrime() which uses ViewModel to add a crime (Room, Repository pattern), onCrimeClicked() callback to MainActivity.
MainActivity launches CrimeDetailFragment which shows either an existing or empty (new) crime for us to fill. We fill it and click back, crime gets saved: CrimeDetailFragment - onStop() { super.onStop; crimeDetailViewModel.saveCrime(crime) }
Database gets updated, CrimeListFragment observes database-change, updateUI() gets called, database is not empty so alert SHOULDN'T pop up but it does.
I click Create again, create second crime, tap back and the alert won't show again.
In other words the alert gets shown one time too many.
Logcat shows this:
`Retrieved 0 crimes`
`empty crime list, show empty dialog`
`show empty dialog`
`list submitted`
`*(I add a crime)*`
`new crime added`
`Retrieved 0 crimes` <--- Why? I just created a crime, Observer should notify and `updateUI()` should get called with a non-empty list
`empty crime list, show empty dialog`
`show empty dialog`
`list submitted`
`Retrieved 1 crimes.` <--- Correct behavior from here on out
Why does my dialog pop up twice instead of once?
This is due to how LiveData works: it caches and returns the last value before querying for updated data.
The first time your CrimeListFragment starts to observe the crimeListLiveData, it gets an empty list, correctly showing your dialog.
When you go to CrimeDetailFragment, the crimeListViewModel.crimeListLiveData is not destroyed. It retains the existing value - your empty list.
Therefore when you go back to your CrimeListFragment, onCreateView() runs again and you start observing again. LiveData immediately returns the cached value it had and Room asynchronously kicks off a query for updated data. Therefore it is expected that you first get an empty list before getting an updated, non-empty list.
You'll see the same behavior if you rotate your device while your EmptyAlertFragment is on the screen and the CrimeListFragment is behind it - you'll end up creating a second copy of your EmptyAlertFragment for the same reason. Then a third, fourth, fifth, etc. if you continue to rotate your device.
As per the Material design guidelines for dialogs, dialogs are for critical information or important decisions, so perhaps the most appropriate solution for your "Create a new crime" requirement is to not use a dialog at all, instead using an empty state in your CrimeListFragment alongside a Floating Action Button. Then, your updateUI method would simply switch between the empty state and your non-empty RecyclerView based on the count.
The other option is that your CrimeListFragment should keep track of whether you've displayed the dialog already in a boolean field, saving that boolean into the Bundle in onSaveInstanceState() to ensure it survives rotation and process death / recreation. That way you can be sure you only show the dialog just a single time for a given CrimeListFragment.
In my application i want show message when fragment has show.
I used viewPager and BottomNavBar for show 4 fragments!
I want when click on BottomNavBar items show fragment and i want when visibility fragment show message.
I write below codes :
class HomeRegisteredFragment : Fragment() {
lateinit var toolbarTile: TextView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home_registered, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Initialize
activity?.let {
toolbarTile = it.findViewById(R.id.homePage_toolbarTitle)
}
//Set title
toolbarTile.text = resources.getString(R.string.registered)
context?.let { ContextCompat.getColor(it, R.color.blue_active) }?.let {
toolbarTile.setTextColor(it)
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser) {
Log.e("showFragLog", "Show")
context?.let { Toast.makeText(it, "Show", Toast.LENGTH_SHORT).show() }
}
}
}
In my above codes, when click on my BottomNavBar for show fragment, show me Log message but not show Toast message.
When click on another BottomNavBar items and again click on previous BottomNavBar item, then show Toast message.
I think in first time not initialize context in setUserVisibleHint method.
How can i initialize context for show Toast in every time?
I changed your codes with below codes :
class HomeRegisteredFragment : Fragment() {
lateinit var toolbarTile: TextView
lateinit var handler: Handler
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home_registered, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Initialize
activity?.let {
toolbarTile = it.findViewById(R.id.homePage_toolbarTitle)
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser) {
//Initialize
handler = Handler()
//Set delay
handler.postDelayed({
Toast.makeText(requireContext(),"Show",Toast.LENGTH_SHORT).show()
}, 10)
}
}
}
First you should use requireContext() instead of context() for avoid from memory leak.
For show Toast for every time, you can initialize handler in setUserVisibleHint , then after some delay run your code!
I hope help you
Storing context in a variable is a horrible practive and most of the times leads to memory leaks, use requireContext() this method was introduced in Support Library 27.1.0. Nowdays most likely you will have a newer version or even using androidx so there is no excuse for storing a context
If you are looking for application context to show the toast message, try the below way and see if it works. Also, initialize it onCreate method so you have the activity context at that point.
val appContext = context!!.applicationContext
O have a similar trouble here. I have one Activity with multiple Fragments, and I need a ListView to show some employes.
But when I call the Adapter class, I don't know how to pass the context variable:
binding.listviewCoordenacoes.isClickable = true
binding.listviewCoordenacoes.adapter = CoordenadorAdapter(requireContext().applicationContext as Activity, arrayListCoordenador)
binding.listviewCoordenacoes.setOnClickListener{}
In the examples in general, it works in Activities. If not possible, I will create an Activity and put it in that.
So right now, when I go to Fragment B from Fragment A, and show a dialog in the onCreate() of Fragment B, and then dismissed it (by clicking on the close button of the dialog) and go back to previous fragment(Fragment A) the dialog appears again for some reason(in Fragment A). If I repeat the action for example 5 times and go back each of those 5 times (to the previous fragment[Fragment A]) the dialog appears 5 times in a row in the previous fragment(Fragment A) . For some reason he is recording the dialogs that are being shown.
So my dialog code is this:
fun Fragment.showDialog(fragment: DialogFragment, tag: String) {
val ft = fragmentManager?.beginTransaction()
val dialog = fragmentManager?.findFragmentByTag(tag) as? DialogFragment
dialog?.let {
ft?.remove(it)
}
dialog.
ft?.addToBackStack(null)
fragment.show(ft, tag)
}
My call to the dialog in the fragment is this:
showDialog(SuccessDialog.newInstance(), SuccessDialog.TAG)
My success dialog fragment is this:
class SuccessDialog : DialogFragment() {
companion object {
const val TAG = "SUCESS_DIALOG"
fun newInstance() = SuccessDialog()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.dialog_success, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent)
dialog?.window?.setDimAmount(0.8f)
closeButton.setOnClickListener {
dialog.cancel()
dialog.dismiss()
}
}
}
This is my activity main back press body:
supportFragmentManager.popBackStack()