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.
Related
I have a custom DialogFragment that I'm using to capture user input that I will create a database entry with. I'm using EditText in an AlertDialog. I am trying to use a single activity for my application and the original tutorial I was studying was using multiple activities and intents but that seems outdated for most cases.
When I debug I find that the EditText is returning "" and is showing up as empty when I call TextUtils.isEmpty() in the MainActivity onDialogPositiveClick.
I've done a lot of combing through the forms here and I'm confused by:
1)many of the answers I find are in Java and not Kotlin
2)many mention onCreate but do not specify onCreateView vs. onCreateDialog or if there's just an onCreate that I need to override.
I have researched this and found answers that confuse me a bit about when and if I need to inflate the layout. This current itteration I didn't inflate it at all. I just set it in the AlertDialog builder.
Maybe it's the interface I'm not understanding. How am I supposed to pass information between the dialog and MainActivity? The interface seems to pass the dialog itself but I seem to be missing something when it comes to getting the EditText from the dialog.
My custom DialogFragment
class NewSongFragment : DialogFragment() {
lateinit var listener: NewSongListener
lateinit var editNewSong: EditText
lateinit var editBPM: EditText
interface NewSongListener {
fun onDialogPositiveClick(dialog: DialogFragment)
fun onDialogNegativeClick(dialog: DialogFragment)
}
/** The system calls this to get the DialogFragment's layout, regardless
of whether it's being displayed as a dialog or an embedded fragment. */
/*
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout to use as dialog or embedded fragment
return inflater.inflate(R.layout.fragment_new_song, container, false)
}
*/
// Override the Fragment.onAttach() method to instantiate the NoticeDialogListener
override fun onAttach(context: Context) {
super.onAttach(context)
// Verify that the host activity implements the callback interface
try {
// Instantiate the NoticeDialogListener so we can send events to the host
listener = context as NewSongListener
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException((context.toString() +
" must implement NewSongListener"))
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
// Use the Builder class for convenient dialog construction
val builder = AlertDialog.Builder(it)
//add inflater
//val inflater = requireActivity().layoutInflater;
//val view = inflater.inflate(R.layout.fragment_new_song, null)
builder
.setView(R.layout.fragment_new_song)
.setCancelable(true)
.setNegativeButton(R.string.cancel,DialogInterface.OnClickListener { dialog, id ->
dialog?.cancel()
})
.setPositiveButton(R.string.button_save,
DialogInterface.OnClickListener {dialog, _ ->
listener.onDialogPositiveClick(this)
})
// Create the AlertDialog object and return it
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}
My MainActivity
class MainActivity : AppCompatActivity(),NewSongFragment.NewSongListener {
private val songViewModel: SongViewModel by viewModels {
SongViewModelFactory((application as SongApplication).repository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//create view
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
val adapter = ItemAdapter(this,
ItemAdapter.OnClickListener { rating -> songViewModel.insertRating(rating) }
)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
//initialize data
songViewModel.allSongs.observe(this) { song ->
// Update the cached copy of the songs in the adapter.
song.let { adapter.submitList(it) }
}
// Use this setting to improve performance if you know that changes
// in content do not change the layout size of the RecyclerView
recyclerView.setHasFixedSize(true)
//add song button
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
showNewSongDialog()
}
}
private fun showNewSongDialog() {
// Create an instance of the dialog fragment and show it
val dialog = NewSongFragment()
dialog.show(supportFragmentManager, "NewSongFragment")
}
override fun onDialogPositiveClick(dialog: DialogFragment) {
// User touched the dialog's positive button
val editNewSong = dialog.view?.findViewById<EditText>(R.id.newSongTitle)
val editBPM = dialog.view?.findViewById<EditText>(R.id.newSongBpm)
if(TextUtils.isEmpty(editNewSong?.text)){
}else{
val newSong = Song(editNewSong?.text.toString(),100)
songViewModel.insertSong(newSong)
val rating = Rating(System.currentTimeMillis(),newSong.songTitle, 50)
songViewModel.insertRating(rating)
}
}
override fun onDialogNegativeClick(dialog: DialogFragment) {
// User touched the dialog's negative button
}
}
You are adding the layout with a resource identifier, so your call to get the view is returning null. (Why? The view is inflated internally and just handled differently.) Since you are using the AlertDialog to collect data, you will have to add an inflated view.
I am also going to suggest that you change the interface to hide the details of the dialog; There is no reason for the main activity to know the internal structure of the dialog. It just needs the song title and BPM and maybe some other stuff. You will find the code a little easier to understand and maintain.
Here is a slight rework. This code just captures the song title, but it can easily be extended to include other data as well.
In NewSongFragment:
interface NewSongListener {
fun onDialogPositiveClick(songTitle: String)
fun onDialogNegativeClick(dialog: DialogFragment)
}
val inflater = requireActivity().layoutInflater;
val view = inflater.inflate(R.layout.fragment_new_song, null)
builder
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, DialogInterface.OnClickListener { dialog, id ->
dialog?.cancel()
})
.setPositiveButton(R.string.button_save)
{ dialog, _ ->
Log.d("Applog", view.toString())
val songTitle = view?.findViewById<EditText>(R.id.newSongTitle)?.text
listener.onDialogPositiveClick(songTitle.toString())
}
In MainActivity.kt
override fun onDialogPositiveClick(songTitle: String) {
// songTitle has the song title string
}
Android dialogs have some quirks. Here are a number of ways to do fragment/activity communication.
Because you are adding the dialog as a Fragment, you should use onCreateView to inflate the view, rather than trying to add a view in onCreateDialog.
I'm building an Android app that has different pages that mainly have some EditText. My goal is to handle the click on the EditText and shows a DialogAlert with an EditText, then the user can put the text, click "save" and the related field in the database (I'm using Room and I've tested the queries and everything works) will be updated. Now I was able to handle the text from the DialogFragment using interface but I don't know how to say that the text retrieved is related to the EditText that I've clicked. What is the best approach to do this?
Thanks in advance for your help.
Let's take this fragment as example:
class StaticInfoResumeFragment : Fragment(), EditNameDialogFragment.OnClickCallback {
private val wordViewModel: ResumeStaticInfoViewModel by viewModels {
WordViewModelFactory((requireActivity().application as ManagementCinemaApplication).resumeStaticInfoRepo)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val root = inflater.inflate(R.layout.fragment_static_info_resume, container, false)
wordViewModel.resumeStaticInfo.observe(viewLifecycleOwner) { words ->
println("test words: $words")
}
val testView = root.findViewById<TextInputEditText>(R.id.textInputEditText800)
testView.setOnClickListener{
val fm: FragmentManager = childFragmentManager
val editNameDialogFragment = EditNameDialogFragment.newInstance("Some Title")
editNameDialogFragment.show(fm, "fragment_edit_name")
}
resumeStaticInfoViewModel.firstName.observe(viewLifecycleOwner, Observer {
testView.setText(it)
})
return root
}
override fun onClick(test: String) {
println("ciao test: $test")
wordViewModel.updateFirstName(testa)
}}
Then I've the ViewModel:
class ResumeStaticInfoViewModel(private val resumeStaticInfoRepo: ResumeStaticInfoRepo): ViewModel() {
val resumeStaticInfo: LiveData<ResumeStaticInfo> = resumeStaticInfoRepo.resumeStaticInfo.asLiveData()
fun updateFirstName(resumeStaticInfoFirstName: String) = viewModelScope.launch {
resumeStaticInfoRepo.updateFirstName(resumeStaticInfoFirstName)
}
....
And the DialogFragment:
class EditNameDialogFragment : DialogFragment() {
private lateinit var callback: OnClickCallback
interface OnClickCallback {
fun onClick(test: String)
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
callback = parentFragment as OnClickCallback
} catch (e: ClassCastException) {
throw ClassCastException("$context must implement UpdateNameListener")
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val title = requireArguments().getString("title")
val alertDialogBuilder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
alertDialogBuilder.setTitle(title)
val layoutInflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val alertCustomView = layoutInflater.inflate(R.layout.alert_dialog_edit_item, null)
val editText = alertCustomView.findViewById<EditText>(R.id.alert_edit)
alertDialogBuilder.setView(alertCustomView)
alertDialogBuilder.setPositiveButton(
"Save",
DialogInterface.OnClickListener { dialog, which ->
callback.onClick(editText.text.toString())
})
alertDialogBuilder.setNegativeButton("No") { _: DialogInterface, _: Int -> }
return alertDialogBuilder.create()
}
companion object {
fun newInstance(title: String?): EditNameDialogFragment {
val frag = EditNameDialogFragment()
val args = Bundle()
args.putString("title", title)
frag.arguments = args
return frag
}
}
}
Do you mean you just want to show a basic dialog for entering some text, and you want to be able to reuse that for multiple EditTexts? And you want a way for the dialog to pass the result back, but also have some way of identifying which EditText it was created for in the first place?
The thing about dialogs is they can end up being recreated (like if the app is destroyed in the background, and then restored when the user switches back to it) so the only real configuration you can do on it (without getting into some complexity anyway) is through its arguments, like you're doing with the title text.
So one approach you could use is send some identifier parameter to newInstance, store that in the arguments, and then pass it back in the click listener. So you're giving the callback two pieces of data in onClick - the text entered and the reference ID originally passed in. That way, the activity can handle the ID and decide what to do with it.
An easy value you could use is the resource ID of the EditText itself, the one you pass into findViewById - it's unique, and you can easily use it to set the text on the view itself. You're using a ViewModel here, so it should be updating automatically when you set a value in that, but in general it's a thing you could do.
The difficulty is that you need to store some mapping of IDs to functions in the view model, so you can handle each case. That's just the nature of making the dialog non-specific, but it's easier than making a dialog for each property you want to update! You could make it a when block, something like:
// you don't need the #ResId annotation but it can help you avoid mistakes!
override fun onClick(text: String, #ResId id: Int) {
when(id) {
R.id.coolEditText -> viewModel.setCoolText(text)
...
}
}
where you list all your cases and what to call for each of them. You could also make a map like
val updateFunctions = mapOf<Int, (String) -> Unit>(
R.id.coolEditText to viewModel::setCoolText
)
and then in your onClick you could call updateFunctions[id]?.invoke(text) to grab the relevant function for that EditText and call it with the data. (Or use get which throws an exception if the EditText isn't added to the map, which is a design error you want to get warned about, instead of silently ignoring it which is what the null check does)
I have made an app in kotlin through the android studio, Now I have used ViewModels to save UI data while phone rotation(configuration change), i also used onSaveInstanceState to save data while pressing back button but it's not working.
The code is below
fragOne.kt
class fragOne : Fragment() {
private lateinit var viewModel: fragViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
if(savedInstanceState!=null){
with(savedInstanceState) {
viewModel.num=getInt("number")
}
}
// Inflate the layout for this fragment
var binding = DataBindingUtil.inflate<FragmentFragoneBinding>(
inflater,
R.layout.fragment_fragone,
container,
false
)
viewModel = ViewModelProviders.of(this).get(fragViewModel::class.java)
// function to update number
fun updateNumber()
{
binding.number.text="${viewModel.num}"
}
updateNumber()
// setting on Click listener for add button
binding.add.setOnClickListener()
{
viewModel.addFive()
updateNumber()
}
// setting on on Click Listener for minus button
binding.minus.setOnClickListener()
{
viewModel.minusOne()
updateNumber()
}
return binding.root
}
override fun onSaveInstanceState(outState: Bundle) {
// Save the user's current game state
outState?.run {
putInt("number",viewModel.num)
}
// Always call the superclass so it can save the view hierarchy state
if (outState != null) {
super.onSaveInstanceState(outState)
}
}
}
ViewModelclass
class fragViewModel:ViewModel()
{
// Initializing num=0
var num=0
// Functions to add five or subtract one
fun addFive()
{
num=num+5
}
fun minusOne()
{
num=num-1
}
}
please tell me because data is not saved when I press back
You can override onBackPressed to do your state saving:
How to implement onBackPressed() in Fragments?
Remember to call super, so that is does also do the back command!
You could also do like the below:
// This callback will only be called when MyFragment is at least Started.
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
// Handle the back button event
}
Really good read: https://developer.android.com/guide/navigation/navigation-custom-back
Back navigation is how users move backward through the history of screens they previously visited. All Android devices provide a Back button for this type of navigation, so you should not add a Back button to your app’s UI. Depending on the user’s Android device, this button might be a physical button or a software button.
Ref:
How to show warning message when back button is pressed in fragments
Example:
Ensure your Activity extends AppCompatActivity
class MyFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(fragViewModel::class.java)
val prefs = activity.getSharedPreferences("Key")
int num = prefs.get("number", -999)
if(num != -999) {
viewModel.num = num
}
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
prefs.edit().putInt("number", viewModel.num).apply()
}
}
...
}
I have rigged up a simple login fragment with a view model.
Here is the fragment :
class LoginFragment : Fragment() {
companion object {
fun newInstance() = LoginFragment()
}
private lateinit var viewModel: LoginViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.login_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)
viewModel.loginState.observe(this, Observer{
handleState(it)
})
login_button.setOnClickListener {
viewModel.isUserValid(username.text.toString(), pass.toString())
}
}
private fun handleState(status: RegisterState) {
if (status.statusMessage.equals("Good"))
view?.findNavController()?.navigate(R.id.action_registerFragment_to_homeFragment)
else
Snackbar.make(login_container, "Welcome to SwA", Snackbar.LENGTH_LONG).show();
}
}
and here is my view model :
class LoginViewModel : ViewModel() {
lateinit var auth: FirebaseAuth
private var _loginState = MutableLiveData<LoginState>()
val loginState : MutableLiveData<LoginState> get() = _loginState
init {
loginState.value = LoginState()
}
fun isUserValid(email: String, password: String): Boolean {
//Add call to authenticate through firebase
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener {
if (it.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
val user = auth.currentUser
//updateUI(user)
} else {
// If sign in fails, display a message to the user.
_loginState.value?.statusMessage = "Authentication Failed"
}
}
return true
}
}
This works and registers a change to the string status when a failed log in is attempted, however it also submits an onChange() when loading the fragment causing the snackbar to appear in the UI before they have actually entered anything when the fragment is created. How can I initialize the view state without triggering an onChange() ?
LiveData class has a method
boolean shouldBeActive() {
return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
}
which checks if owner of the LifeCycle, Activity or Fragment, is on a state after STARTED which means for an Activity if it has called onStart().
So whenever an Activity or Fragment observes a LiveData it gets LiveData's value by setValue or PostValue after onStart is called.
One of the ways to prevent same value to update UI more than once is to use SingleLiveEvent class, example here. Another one is using Event class, example is here.
I have an activity using fragments. To communicate from the fragment to the activity, I use interfaces. Here is the simplified code:
Activity:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
private val diaryFragment: DiaryFragment = DiaryFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
}
The fragment:
class DiaryFragment: Fragment() {
private var onEntryClickedListener: IAddEntryClickedListener? = null
private var onDeleteClickedListener: IDeleteClickedListener? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_diary, container, false)
//Some user interaction
onDeleteClickedListener!!.onEntryDeleteClicked()
onDeleteClickedListener!!.onEntryDeleteClicked()
return view
}
interface IAddEntryClickedListener {
fun onAddEntryClicked()
}
interface IDeleteClickedListener {
fun onEntryDeleteClicked()
}
fun setOnEntryClickedListener(listener: IAddEntryClickedListener) {
onEntryClickedListener = listener
}
fun setOnDeleteClickedListener(listener: IDeleteClickedListener) {
onDeleteClickedListener = listener
}
}
This works, but when the fragment is active and the orientation changes from portrait to landscape or otherwise, the listeners are null. I can't put them to the savedInstanceState, or can I somehow? Or is there another way to solve that problem?
Your Problem:
When you switch orientation, the system saves and restores the state of fragments for you. However, you are not accounting for this in your code and you are actually ending up with two (!!) instances of the fragment - one that the system restores (WITHOUT the listeners) and the one you create yourself. When you observe that the fragment's listeners are null, it's because the instance that has been restored for you has not has its listeners reset.
The Solution
First, read the docs on how you should structure your code.
Then update your code to something like this:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
// DO NOT create new instance - only if starting from scratch
private lateinit val diaryFragment: DiaryFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
// Null state bundle means fresh activity - create the fragment
if (savedInstanceState == null) {
diaryFragment = DiaryFragment()
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
else { // We are being restarted from state - the system will have
// restored the fragment for us, just find the reference
diaryFragment = supportFragmentManager().findFragment(R.id.content_frame)
}
// Now you can access the ONE fragment and set the listener on it
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
}
}
Hope that helps!
the short answer without you rewriting your code is you have to restore listeners on activiy resume, and you "should" remove them when you detect activity losing focus. The activity view is completely destroyed and redrawn on rotate so naturally there will be no events on brand new objects.
When you rotate, "onDestroy" is called before anything else happens. When it's being rebuilt, "onCreate" is called. (see https://developer.android.com/guide/topics/resources/runtime-changes)
One of the reasons it's done this way is there is nothing forcing you to even use the same layout after rotating. There could be different controls.
All you really need to do is make sure that your event hooks are assigned in OnCreate.
See this question's answers for an example of event assigning in oncreate.
onSaveInstanceState not working