How can I code buttons to change layouts in Kotlin? - android

I am trying to make a text-based choice game for Android, very similar to the game called "Magium"
https://play.google.com/store/apps/details?id=com.magiumgames.magium&hl=en
Users will choose from a few options by pressing respective buttons to progress the story. After a buttons press, I want the app to show another layout. Let's say there is only one button (named button1) in page1. Pressing it changes the layout to page2. I achieved this by:
button1.setOnClickListener {
setContentView(R.layout.page2)
}
Again, for simplicity, let's say there is only one button (named button2) in page2. I want to show page3 after user presses button2.
I added the same code below the previous code as:
button1.setOnClickListener {
setContentView(R.layout.page2)
}
button2.setOnClickListener {
setContentView(R.layout.page3)
}
However, the app doesn't open when I do this. I am just beginning to learn programming. Could someone point me in the right direction?

You should not, in most cases, call setContentView() multiple times. You can read more in this answer.
Is there a point to change the whole layout to only update the content of it? You should be updating the content of your UI when the user clicks, not inflating another layout.

Did you check if the clickListener for the second button is called?
Maybe if you set the click listener before calling setContentView(R.layout.page2) is will not have the button displayed for you to register to it.
But i agree with alvarezfmb, you should use a better solution for changing the views, maybe Fragments or even hiding and showing views is better than this solution.

You could use fragment.
First, use in your activity layout as a container of Fragment, here we give it a id like, android:id="+id/fragment_container"
Second, create a layout xml file for your fragment, here we give it a name, like fragment_page_3.xml
Then create a subclass of Fragment, and override its onCreateView() method, where you inflate your fragment's layout. Code here
class Page3Fragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_page_3, container, false)
}
}
Finally, in clickListener, you could use fragmentManager load your fragment, code like this:
button2.setOnClickListener {
val fragment = Page3Fragment()
val fm = supportFragmentManager
fm.beginTransaction()
.add(R.id.fragment_container,fragment)
.commit()
}
I just want to give you some ideas.
You cannot write code like this, this is wrong implementation for your app because you may need several fragment and you need a Callbacks interface implemented by your activity.
If you could change your content of UI, it's a better way.
To learn more, you could refer https://developer.android.com/guide/components/fragments

Related

How to handle Fragment to not load again when coming from another fragment in Navigation Component in Android?

Scenario: I have 2 fragments ProductList and ProductDetail in my nav graph. And when i click on any product it opens the ProductDetail fragment using findNavController.navigate() method.
Problem: The problem is when I go back from ProductDetail to ProductList fragment, the ProductList fragment reloads again and it also calls the api to fetch products list, which I want to avoid.
If anyone knows the reason behind it or the solution to this particular issue please let me know in comments.
You should cache the fetch result locally, pull from the cache either during a fetch attempt or upon failure of a fetch attempt. This is a very common pattern on mobile.
Finally I got the answer from somewhere, so I am posting the sample code to resolve the issue I am facing -
private lateinit var contentView: View
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
if (!::contentView.isInitialized) {
binding = FragmentNewProductListBinding.inflate(layoutInflater)
contentView = binding.root
// initialize your views or set click listeners or call apis
}
return contentView
}
Note: To elaborate, there is some internal bug in the navigation library where it draws the previous fragment from scratch when going back from another fragment. So as a workaround or temporary patch, what we can do is just check whether the view is already initialized or not, if yes then don't create it again.
Hope you understand the reasoning and answer.

RecyclerView with StaggeredGridLayoutManager in ViewPager, arranges items automatically when going back to fragment

I am using Navigation component in my App, using google Advanced Sample(here).
my problem is when going back to a fragment, the scrolling position does not lost but it rearranges items and moves highest visible items so that top of those item align to top of recyclerview. please see this:
before going to next fragment:
and after back to fragment:
this problem is matter because some times clicked item goes down and not seen until scroll down.
how to prevent this behavior?
please consider:
this problem exist if using navigation component to change fragment. if start fragment using supportFragmentManager.beginTransaction() or start another activity and then go to this fragment it is OK. but if I navigate to another fragment using navigation component this problem is exist.(maybe because of recreating fragment)
also this problem exist if using fragment in ViewPager. i.e recyclerView is in a fragment that handle with ViewPagerAdapter and viewPager is in HomeFragment that opened with Navigation component. if recyclerView is in HomeFragment there is no problem.
no problem with LinearLayoutManager. only with StaggeredGridLayoutManager.
there is not difference if using ViewPager2 and also FragmentStatePagerAdapter
I try to prevent recreate of fragment(by this solution) but not solved.
UPDATE:
you can clone project with this problem from here
When using Navigation Component + ViewPager + StaggeredGridLayoutManager, wrong recyclerView.computeVerticalScrollOffset() has been returned during Fragment recreate.
In general, all layout managers bundled in the support library already know how to save and restore scroll position, but in this case, we had to take responsibility for this.
class TestFragment : Fragment(R.layout.fragment_test) {
private val testListAdapter: TestListAdapter by lazy {
TestListAdapter()
}
private var layoutManagerState: Parcelable? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postListView.apply {
layoutManager = StaggeredGridLayoutManager(
2, StaggeredGridLayoutManager.VERTICAL
).apply {
gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
}
setHasFixedSize(true)
adapter = testListAdapter
}
testListAdapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT
}
override fun onPause() {
saveLayoutManagerState()
super.onPause()
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
restoreLayoutManagerState()
}
private fun restoreLayoutManagerState () {
layoutManagerState?.let { postListView.layoutManager?.onRestoreInstanceState(it) }
}
private fun saveLayoutManagerState () {
layoutManagerState = postListView.layoutManager?.onSaveInstanceState()
}
}
Source code: https://github.com/dautovicharis/MyStaggeredListSample/tree/q_65539771
The Navigation Component behavior is normal when you navigate from one fragment to another. I mean, onDestroyView() method from the previous fragment is executed, so it means that your view is destroyed, but not the fragment. Remembering that fragment has two lifecycles one for the fragment and another one for the view, There was a video about it.
Also, there were issues registered in issue tracker in order to avoid this behavior in some cases and the GitHub issues:
https://issuetracker.google.com/issues/127932815
https://github.com/android/architecture-components-samples/issues/530
The problem is that when you have fragment that is heavy to recreate, is easier to do not destroy it and just add one fragment. So, when you go back it is not recreated. But, for this behavior is not part of navigation component.
Solutions
The easiest solution is to not use navigation component and work with the tradicional way, as you can see this works perfectly in you use case.
You can use the traditional way just for this use case, and use the navigation component for other cases.
You can inflate this view in an activity. So you are adding un activity
But if the previous tree options is not possible. You can try the following:
If you are using viewModel, you can use SaveState. Basically, it can save the data from your fragment, it is like a map data structure, so you can save positions from your list or recycler view. When go back to this fragment, get the position from this saveState object and use scrollToPosition method in order to add the real position.
Recycler view have methods for restore positions. You can see the uses cases for that, because first you need the data and then add the real position, for more details you can visit this link. This configuration for recycler view is useful also when you lose memory and you need to recreate the recycler view with asynchronous data.
Finally, if you want to understand more about how fragment works with navigation component, you can see this link

why my progress bar from my activity doesn't show in my fragment after I press hardware back button?

so I am using navigation controller component in Android. I have a progress bar in my MainActivity that will be used in all my fragments when the user need to wait while fetching data from server.
in my onCreate MainActivity it will be declared like this:
progressBar = findViewById(R.id.progressBar_main_activity)
and in my FragmentA, it will be declared like this :
lateinit var mActivity : FragmentActivity
lateinit var progressBar : ProgressBar
override fun onAttach(context: Context) {
super.onAttach(context)
activity?.let {
mActivity = it
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
fragmentView = inflater.inflate(R.layout.fragment_user_control, container, false)
progressBar = mActivity.progressBar_main_activity
return fragmentView
}
override fun onResume() {
super.onResume()
progressBar.visibility = View.VISIBLE
}
let say I have 2 fragments
I navigate from FragmentA to FragmentB. using the code below
val eventDetailDestination = UserControlFragmentDirections.actionGlobalDestinationEventDetail(selectedEvent)
Navigation.findNavController(fragmentView).navigate(eventDetailDestination)
in FragmentB, after the user do some actions in FragmentB, then they need to go back to FragmentA
here is the problem ....
if the user goes back from FragmentB to FragmentA using back button in the top left corner in action bar/toolbar, the progress bar in FragmentA will show up.
but if the user goes back using hardware back button in the bottom right, the progress bar in FragmentA will never show. even though I am sure progressBar.visibility = View.VISIBLE has been executed in FragmentA ?
I have tried to read the difference between back button in toolbar and hardware back button. but I have no Idea why this happened. please help :)
That happens because fragmentA is getting deleted and recreated from scratch when using the hardware back button which is the expected result. You can override the default behaviour like below:
In MainActivity
override fun onSupportNavigateUp(): Boolean {
//Use component backstack pop
return Navigation.findNavController(fragmentView).navigateUp()
}
Sheding some more light
Up vs. Back
The Up button is used to navigate within an app based on the
hierarchical relationships between screens. For instance, if screen A
displays a list of items, and selecting an item leads to screen B
(which presents that item in more detail), then screen B should offer
an Up button that returns to screen A.
If a screen is the topmost one in an app (that is, the app's home), it
should not present an Up button.
..
The system Back button is used to navigate, in reverse chronological
order, through the history of screens the user has recently worked
with. It is generally based on the temporal relationships between
screens, rather than the app's hierarchy.
When the previously viewed screen is also the hierarchical parent of
the current screen, pressing the Back button has the same result as
pressing an Up button—this is a common occurrence. However, unlike the
Up button, which ensures the user remains within your app, the Back
button can return the user to the Home screen, or even to a different
app.
Reference:
Android Navigation with Back and Up

Android MutableLiveData keeps emitting when I re-enter fragment

I'm using a shared ViewModel in Navigation component rather than creating a ViewModel for every fragment (mostly because it's easier) but now I have a problem when I re-enter a fragment and subscribe to the ViewModel live data of that fragment, I get the last state also too.
here is the ViewModel Code:
val apiLessonData: MutableLiveData<String>> = MutableLiveData()
fun getLessonsUserCreated() =
apiCall(MyMaybeObserver(apiLessonData))
in MyMaybeObserver, I have somthing like this:
override fun onSuccess(t: T) {
apiDataObserver.postValue(t)
}
and this is how I observe it in my fragment:
private val apiAddGoalData = Observer<String> { response ->
showSnack(response)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
.
.
viewModel.apiAddGoalData.observe(viewLifecycleOwner, apiAddGoalData)
.
.
}
now when I enter the first time it works fine but I open it the second time, it shows the snack from the previous time, how to stop this without creating new ViewModel?
In the simple way You could set null for your MutableLiveData after getting data in onchange method of the observer. For more information you can read this article:livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case
. also you can see this question maybe help you: How to clear LiveData stored value?
I don't think your problem is with the LiveData since you are wisely using the viewLifecycleOwner, problem is with the state of the view and lifeCycle of the fragment. With navigation component of jetpack, fragments get replaced in the container. Think of this scenario: You open fragment A then you navigate to frament B and press back button to return to fragment A. onCreateView and onViewCreated methods of the fragment A gets called again. Since the onDestroy of fragment A haven't been called when you opened fragment B some of the view states will be restored while returning to A. This is as you might know the same reason we use viewLifecycleOwner. So Nullify or clear the state of the views in the onDestroyView of the fragment A:
recyclerView.setAdapter(null)
checkBox.setChecked(false)

Navigation Component Lifecycle

I have been trying out Navigation Component for a while now but somehow not getting the reason (or explanation) behind current Lifecycle of Navigation Component. Here are some points that needs clarification.
1. Fragment to Fragment flow
In navigation Component every fragment (or lets say page) is recreated every time it is visited (or revisited). So, when you are at A and go to B, A is destroyed and later when you go back to A (like pressing back button) A is created from stretch.
In a traditional Activity patterns when you go back to A it just goes to onResume as it wasn't destroyed when moving to B. Any reason that this pattern is changed in Navigation Component?
The problem of recreating is when you have a lot of data and it takes time to get redrawn and it feels like app is freezing. An example would be simple HomePage (say Facebook NewsFeed). Preserving data can be handled with ViewModel but drawing of all of the feeds again require time and it will freeze.
There is another problem that recreation generates. Assume this scenario: A has an Collapsing AppBar with a NestedScrollView. User scrolls down and AppBar will collapse and then user moves to a different page B. When he comes back to A it will be redrawn and AppBar is expanded. I am not sure if it is a bug or I should do something to fix it? So any solution here is also welcomed.
2. Activity recreation
If activity is recreated for certain reasons like orientation change, every page in Navigation Component gets recreated until current destination. Although onCreate method of Fragment not onCreateView is getting called, I don't see a point of creating Fragments in Activity recreation. Any explanation would be welcomed.
Navigation component only supports fragment replacement as of now. So you won't be able to add() a fragment as you do it with Manual fragment transaction.
However, if your worry is about re-inflating the layout and re-fetching the data for the fragment, it could be easily resolved with below two methods.
Once the view is created, store it in a variable and use it whenever onCreateView() is called.
private var view: View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (view == null) {
view = inflater.inflate(R.layout.fragment_list, container, false)
//...
}
return view
}
Source: https://twitter.com/ianhlake/status/1103522856535638016
Use ViewModel with the Fragment and hold the data required as a member variable. By this way, the data is not cleared when you replace the associated fragment. The ViewModel gets cleared only on onDestroy() of the fragment, which will only happen when you destroy the parent activity. https://developer.android.com/images/topic/libraries/architecture/viewmodel-lifecycle.png
The way that we use fragments to bridge data and views has changed slightly, and in a good way, when migrating to the Navigation library. It forces us to distinguish between Fragment and View lifecycles.
Pre-navigation: observe LiveData in onCreate() using Fragment's lifecycleScope.
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
...
import kotlinx.android.synthetic.main.old_fragment.*
class OldFragment : Fragment(R.layout.old_fragment) {
private val vm by activityViewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
vm.getLiveData().observe(this) { data ->
oldTextView.text = data.name
}
}
}
Navigation: observe LiveData in onViewCreated() using viewLifecycleOwner scope.
...
class NewFragment : Fragment() {
private val vm by activityViewModels<MainViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
vm.getLiveData().observe(viewLifecycleOwner) { data ->
oldTextView.text = data.name
}
}
}
Key Notes:
Not all Lifecycle Owners are the same. The fragment lifecycle will not execute the observer when the view is recreated (while using Navigation library and navigating up/back).
The viewLifecycleOwner cannot be accessed before the view is created.
Hopefully, this can help prevent replacement of LiveData code as developers migrate to Navigation.

Categories

Resources