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()
}
}
...
}
Related
Is it possible to share variable values between Fragments and RecyclerView Adapters? I am currently using SharedPreferences to store data persistently.
I am currently working on a Save button that upon clicking, it will save the article in the Room database. Each itemview in the recyclerview has a save button, I am having trouble updating the UI of the button upon clicking it.
For example, the button is supposed to have a default text save, upon clicking it once, it saves the data and changes the Button text to saved.
In the Activity below I initialized the preferences variable
MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var preferences:SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
preferences = getSharedPreferences("file",Context.MODE_PRIVATE)
}
}
This is the fragment where I use the RecyclerView. In the code below I am resolving the logic part of the button, like for instance when the button is clicked it is saved in the Database/displaying Toast.
That is when the main problem comes as I am having difficulty syncing the UI and the Logic aspect of the Button. The if-statement helps with the states of the button
BreakingNewsFragment
class BreakingNews: Fragment(R.layout.breakingnews) {
lateinit var Main:SharedPreferences
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Main = (activity as MainActivity).preferences //Initializing the Main lateinit var
val editor = Main.edit()
newsAdapter.savedOnClickListener {
if(Main.getInt("numbers",0) == 0)
{
editor.putInt("Adapter",0).apply()
Toast.makeText(activity, "Article Successfully saved", Toast.LENGTH_SHORT).show()
editor.putInt("numbers",1).apply()
viewModel.saveArticle(it)
}else if(Main.getInt("numbers",0) == 1) {
editor.putInt("Adapter",1).apply()
android.widget.Toast.makeText(activity, "Article is already saved", android.widget.Toast.LENGTH_SHORT).show()
editor.putInt("numbers",2).apply()
}else if(Main.getInt("numbers",0) == 2){
editor.clear().apply()
android.widget.Toast.makeText(activity, "Restart", android.widget.Toast.LENGTH_SHORT).show()
}
}
}
}
This is where I will handle the UI portion of the Button, I can't find a way to share variable values between the Adapter and the Fragment so they can be in sync.
NewsAdapter.kt
class NewsAdapter: RecyclerView.Adapter<NewsAdapter.ArticleViewHolder>() {
class ArticleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val button = itemView.findViewById<Button>(R.id.button)}
fun savedOnClickListener(listen:(Article) -> Unit) {
SavedonItemClickListener = listen
}
private var SavedonItemClickListener: ((Article) -> Unit)? = null
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
val article = differ.currentList[position]
holder.button.setOnClickListener {
Log.e("Adapter","Before savedOnITEM")
SavedonItemClickListener?.let {
Log.e("Adapter","After if")
it(article)
}
}
}
}
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.
I have the single activity with several fragments on top, as Google recommends. In one fragment I wish to place a switch, and I wish to still know it's state when I come back from other fragments. Example: I am in fragment one, then I turn on the switch, navigate to fragment two or three, go back to fragment one and I wish to load that fragment with that switch in the on position as I left it.
I have tried to copy the examples provided by google advocates, just to see the code to fail hard and do nothing.
/////////////////////////////////////////////////////////////////
//Inside the first fragment:
class myFragment : Fragment() {
companion object {
fun newInstance() = myFragment()
}
private lateinit var viewModel: myViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.my_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
**viewModel = ViewModelProvider(this, SavedStateVMFactory(this)).get(myViewModel::class.java)
//Here I was hoping to read the state when I come back.
switch_on_off.isChecked = viewModel.getSwRoundTimerInit()**
subscribeToLiveData() //To read liveData
switch_on_off.setOnCheckedChangeListener { _, isChecked ->
viewModel.setOnOff(isChecked)
}
}//End of onActivityCreated
//other code...
/////////////////////////////////////////////////////////////////////////
//On the fragment ViewModel
class myViewModel(private val **mState: SavedStateHandle**) : ViewModel() {
//SavedStateHandle Keys to save and restore states in the App
private val swStateKey = "SW_STATE_KEY"
private var otherSwitch:Boolean //other internal states.
//Init for the other internal states
init {
otherSwitch = false
}
fun getSwRoundTimerInit():Boolean{
val state = mState[swStateKey] ?: "false"
return state.toBoolean()
}
fun setOnOff(swValue:Boolean){
mState.set(swStateKey, swValue.toString())
}
}
This does not work. It always loads the default (off) value, as if the savedState is null all the time.
change
//fragment scope
viewModel = ViewModelProvider(thisSavedStateVMFactory(this)).get(myViewModel::class.java)
to
//activity scope
viewModel = activity?.let { ViewModelProviders.of(it,SavedStateVMFactory(this)).get(myViewModel::class.java) }
https://developer.android.com/topic/libraries/architecture/viewmodel#sharing
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
I'm new on Android, in particular on Kotlin development.
How from title, i'm trying to understand how to achieve this:
I have an Activity with some buttons and textviews. I would to implement an hidden fragment opened after 5 clicks on UI. That fragment work look like the activity. I'm able to open the fragment properly and set the layout properly. I don't know how to replace buttons activity settings from activity to fragment. I have same problem with the textview. How could I achieve it?
Thanks in Advance.
Here Activity Kotlin part that open fragment:
override fun onTouchEvent(event: MotionEvent): Boolean {
var eventaction = event.getAction()
if (eventaction == MotionEvent.ACTION_UP) {
//get system current milliseconds
var time = System.currentTimeMillis()
//if it is the first time, or if it has been more than 3 seconds since the first tap ( so it is like a new try), we reset everything
if (startMillis == 0L || (time-startMillis> 3000) ) {
startMillis=time
count=1
}
//it is not the first, and it has been less than 3 seconds since the first
else{ // time-startMillis< 3000
count++
}
if (count==5) {
// Log.d("tag","start hidden layout")
// Get the text fragment instance
val textFragment = MyFragment()
val mytostring =board_status_tv.toString()
val mArgs = Bundle()
mArgs.putString(BOARDSTATE, mytostring)
textFragment.setArguments(mArgs)
// Get the support fragment manager instance
val manager = supportFragmentManager
// Begin the fragment transition using support fragment manager
val transaction = manager.beginTransaction()
// Replace the fragment on container
transaction.replace(R.id.fragment_container,textFragment)
transaction.addToBackStack(null)
// Finishing the transition
transaction.commit()
}
return true
}
return false
}
Fragment Kotlin class:
class MyFragment : Fragment(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val parentViewGroup = linearLayout
parentViewGroup?.removeAllViews()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Get the custom view for this fragment layout
val view = inflater!!.inflate(R.layout.my_own_fragment,container,false)
// Get the text view widget reference from custom layout
val tv = view.findViewById<TextView>(R.id.text_view)
// val tv1 = view.findViewById<TextView>(R.id.board_status_tv1)
// Set a click listener for text view object
tv.setOnClickListener{
// Change the text color
tv.setTextColor(Color.RED)
// Show click confirmation
Toast.makeText(view.context,"TextView clicked.",Toast.LENGTH_SHORT).show()
}
// Return the fragment view/layout
return view
}
override fun onPause() {
super.onPause()
}
override fun onAttach(context: Context?) {
super.onAttach(context)
}
override fun onDestroy() {
super.onDestroy()
}
override fun onDetach() {
super.onDetach()
}
override fun onStart() {
super.onStart()
}
override fun onStop() {
super.onStop()
}
}
Please note that you will need to get Text before converting it to string, like that in second line.
board_status_tv .getText(). toString()
val textFragment = MyFragment()
val mytostring = board_status_tv.getText().toString()
val mArgs = Bundle()
mArgs.putString(BOARDSTATE, mytostring)
textFragment.setArguments(mArgs)
Hope this will resolve your problem