using the navigation component on two fragments, my app currently destroys a fragment when navigateUp() is called (using the setupActionBarWithNavController()), and therefore when I enter the deleted fragment, all progress is lost, I am trying to change that my adding the fragment to a backstack (and only one instance of the fragment) but I've been struggling with that...
Here's some code:
MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loginRepository = LoginRepository()
if (!loginRepository.checkLoggedInState()) {
goToLoginActivity()
}
Log.d("FirebaseMain", "onCreate: ${loginRepository.checkLoggedInState()}")
//MyApplication.app.currentUser
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
navController = Navigation.findNavController(this, R.id.nav_host_fragment)
// setup custom action bar
setSupportActionBar(findViewById(R.id.toolbar))
// adds a return button in the ActionBar.
NavigationUI.setupActionBarWithNavController(this, navController)
}
FormFragment:
(the fragment I want to save its progress and add to backstack)
class FormFragment : Fragment() {
private lateinit var binding: FragmentFormBinding
private val checkListRecyclerAdapter = CheckListRecyclerAdapter(
hashMapOf(
0 to "mirrors",
1 to "blinkers1",
2 to "blinkers11",
3 to "blin4kers",
4 to "blink3e1123rs",
5 to "blink6ers",
6 to "blin53kers",
7 to "blin8kers",
8 to "blin7kers",
9 to "blin43kers",
10 to "blin32kers",
11 to "blin322kers",
)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_form, container, false)
binding.rvCheckList.apply {
// this is used to load the data sequentially from top to bottom,
this.layoutManager = LinearLayoutManager(context)
// the adapter that loads the data into the list
this.adapter = checkListRecyclerAdapter
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
Thanks in advance for any help!
I've been struggling with some similar to this.
Fragments in Navigation Component doesnt keep their state. If you keep your data progress while navigate, you need create a SharedViewModel scoped to a Navigation Graph.
In Fragment:
private val navGraphScopedViewModel by navGraphViewModels<NavGraphViewModel>(R.id.your_nav_graph)
Create this class:
class NavGraphViewModel : ViewModel() {
var sharedData= CustomObject()
}
And from all your fragments you can access and set data:
val data = navGraphScopedViewModel.sharedData
I hope I've helped you
Related
GameActivity
____FragmentQuest
____FragmentFight
class MapLvl.kt
this is a text RPG Fragment Quest displays a journey through different maps (changing the content in one fragment according to a template) text, pictures, navigation buttons. if there is a mob on the map, then the "Fight" button appears and a fragment of the turn-based fight FightFragment opens (hit the head \ legs\ body protect the head \ legs\ body). after the battle, return to QuestFragment
class MapLvl fills with the content of FightFragment
I need to change QuestFragment from classLvl to FightFragment. how to do it?
it doesn't work:
class MapLvl.kt:
class MapLevels(){
fun changeLvl (bind: FragmentQuestBinding,hero: Hero, activity: GameActivity,db: Maindb) {
when (hero.mapLvl) {
1 -> MapLevels().mapLevel1(bind, activity, hero, db)
2 -> MapLevels().mapLevel2(bind, activity, hero,db)
else -> {}
}
}
fun mapLevel2 (bind: FragmentQuestBinding,activity: GameActivity,hero:Hero,db: Maindb) {
bind.btnAtack.visibility= View.VISIBLE
//the problem is here:
bind.btnAtack.setOnClickListener {
(activity as GameActivity).supportFragmentManager
.beginTransaction()
.replace(R.id.placeHolder,FightFragment.newInstance())
.commit()
}
}
}
error: FragmentManager has not been attached to a host
QuestFragment:
class QuestFragment : Fragment() {
lateinit var bind:FragmentQuestBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
bind = FragmentQuestBinding.inflate(inflater)
return bind.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val hero = Hero()
val db = Maindb.heroSetDb(requireActivity())
hero.extractHeroData(db,hero)
scopeMain.launch {
delay(50)
MapLevels().changeLvl(bind,hero,GameActivity(),db)
}
if you make a call directly from a Fragment, then it works: (but it is necessary not from the fragment but from the class)
QuestFragment:
class QuestFragment : Fragment() {
lateinit var bind:FragmentQuestBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
bind = FragmentQuestBinding.inflate(inflater)
return bind.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val hero = Hero()
val db = Maindb.heroSetDb(requireActivity())
hero.extractHeroData(db,hero)
scopeMain.launch {
delay(50)
MapLevels().changeLvl(bind,hero,GameActivity(),db)
}
bind.btnAtack.setOnClickListener {
(activity as GameActivity).supportFragmentManager
.beginTransaction()
.replace(R.id.placeHolder,FightFragment.newInstance())
.commit()
}
it Work:
class MapLevels(val bind: FragmentQuestBinding,
val hero: Hero,
val db: Maindb,
val activity: FragmentActivity ){
fun changeLvl() {
when (hero.mapLvl) {
1 -> MapLevels(bind,hero,db,activity).mapLevel1()
2 -> MapLevels(bind,hero,db,activity).mapLevel2()
else -> {}
}
}
fun mapLevel2() {
bind.imgLocation.setImageResource(R.drawable.map_loc02)
bind.txtLocationDiscription.text ="text"
bind.btnAtack.visibility= View.VISIBLE
bind.btnAtack.setOnClickListener {
(activity as FragmentActivity).supportFragmentManager
.beginTransaction()
.replace(R.id.placeHolder,FightFragment.newInstance())
.commit()
MapLevels().changeLvl(bind,hero,GameActivity(),db)
It looks like you are creating a new GameActivity instance to pass to your changeLvl() method. YOU SHOULD NEVER DO THIS. The new activity is NOT the same one that currently is displayed on the screen. Instead, you should use the requireAcitivty() to get the fragment's parent activity:
MapLevels().changeLvl(bind,hero,requireActivity(),db)
I can't be sure this will fix your current problem because I'm not even sure what the problem is exactly. But it is one issue that you need to change.
I'm updating my knowledge of Android, and I want to make an app with a Drawer menu, call an api and display the values inside a fragment. Starting from the template created by android studio itself, I have followed this tutorial:https://howtodoandroid.com/mvvm-retrofit-recyclerview-kotlin/ but I have a problem when programming the MainActivity.
Android studio template create this fragment (only changes the name of fragments):
class CheckListFragment : Fragment() {
private var _binding: FragmentCheckListBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val checklistViewModel =
ViewModelProvider(this).get(CheckListViewModel::class.java)
_binding = FragmentCheckListBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textChecklist
checklistViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
The issue is that I don't know how I should call the viewmodel in that section, since I've tried in different ways, reading and reading examples, but none is exactly what I need. If you need me to show more parts of the code (viewmodel, viewmodelfactory etc. I can add it without any problem, although they are practically the same as those described in the tutorial)
In that example, in the MainActivity class we have this:
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var binding: ActivityMainBinding
lateinit var viewModel: MainViewModel
private val retrofitService = RetrofitService.getInstance()
val adapter = MainAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel = ViewModelProvider(this, MyViewModelFactory(MainRepository(retrofitService))).get(MainViewModel::class.java)
binding.recyclerview.adapter = adapter
viewModel.movieList.observe(this, Observer {
Log.d(TAG, "onCreate: $it")
adapter.setMovieList(it)
})
viewModel.errorMessage.observe(this, Observer {
})
viewModel.getAllMovies()
}
But here not use fragments.
I would greatly appreciate help, or a link where I can see an example of this
I have an app with 3 main destinations which can be accessed by a bottom nav view. Each destination has its own navigation graph.
The problem is that when I minimize and reopen my app, the navigation components reset to the default destination. Why does this happen?
My main activity: (Irrelevant code omitted)
class MainActivity : AppCompatActivity() {
// List of base host containers
val fragments = listOf(
HomeHostFragment(),
CoursesHostFragment(),
SearchHostFragment()
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewPager: ViewPager = findViewById(R.id.main_activity_pager)
viewPager.adapter = ViewPagerAdapter()
}
inner class ViewPagerAdapter : FragmentPagerAdapter(supportFragmentManager) {
override fun getItem(position: Int): Fragment =
fragments[position]
override fun getCount(): Int =
fragments.size
}
}
HomeHostFragment.kt:
class HomeHostFragment : Fragment() {
private lateinit var navController: NavController
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.fragment_home_host, container, false)
override fun onStart() {
super.onStart()
navController = requireActivity().findNavController(R.id.nav_host_home)
navController.setGraph(R.navigation.nav_graph_home)
NavigationUI.setupWithNavController(toolbar_home, navController)
}
fun onBackPressed(): Boolean {
return navController.navigateUp()
}
}
Whenever onStart() is called, NavigationUI.setupWithNavController() is called again which resets the navigation. Move this call to onViewCreated() so that the navigation setup is not done every time the fragment pauses and restarts.
Please help me with a problem.
I have the activity_main.xml set up with a score text box, a test button and a fragment container that swaps between 2 fragments.
The fragments are basically containers for buttons.
The "buttonTest" does exactly what "button1" from the fragment does (increments the score) but the test button (located on activity_main.xml) works and the fragment one ... does not
When viewing the logs ...i see that score does update when i click both buttons but with different values.
If i click "buttonTest" it adds 5 to score so score = 5. On another click it adds another 5 so score = 10
If i click in the fragment on "button1" ... calling the same method score is now 1 then 2 then 3.
If i now click "buttonTest" the score is 15.
The problem is that the LiveData keeps separate values depending on where the method was called.
Allso the MainActivity observer does not update on the fragment call... only on the activity one.
Please help.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val scoreSwitch = binding.scoreSwitch
binding.buttonTest.setOnClickListener { viewModel.adaugaTren(5) }
// score observer
viewModel.scor.observe(this, Observer { newScore ->
binding.txtPunctaj.text = newScore.toString()
//todo sterge LOG
Log.d("test","Scor Observer triggers")
})
.......
class MainViewModel: ViewModel(){
private val _score = MutableLiveData(0)
val score: LiveData<Int>
get() = _score
fun adaugaTren(valoare:Int) {
_scor.value = _scor.value?.plus(valoare)
listaTrenuri.add(valoare)
//testing
Log.d("test","Trenuri: ${scor.value}")
updateDisplay()
}
fun updateDisplay(){
// _trenuriDetinute.value = listaTrenuri.toString()
_trenuriDetinute.value = TextUtils.join(", ",listaTrenuri)
//testing
Log.d("test","UpdateDisplay: ${scor.value}")
}
...
}
class TrenuriFragment : Fragment() {
private lateinit var binding: FragmentTrenuriBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater,R.layout.fragment_trenuri, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initButtons()
}
fun initButtons(){
binding.button1.setOnClickListener { viewModel.adaugaTren(1)}
....
}
}
In TrenuriFragment use activityViewModels
// viewModels is scoped in fragment but you need activity
private val viewModel: MainViewModel by activityViewModels()
Im using ViewModel with states and LiveData with one observer .
I have an activity and a fragment . everything works fine until Im rotating the screen
and then, the states I want to observe , clash.
What can I do in order to prevent it and make it work as expected?
I know I can add more observers but I don't want to solve it in this way , it may lead to problems with the other code.
MainActivity code :
private var appsDetailsHmap = HashMap< String , AppsDetails>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
progressBar = CircleProgressBarDialog(this)
viewModel.getState().observe(this, Observer {state->
when(state){
is SettingsViewModelStates.GetAppsDetails->initUI(state.list)
is SettingsViewModelStates.ShowDialog->progressBar.showOrHide(state.visibility)
is SettingsViewModelStates.GetCachedData->setCachedSettings(state.appDetailsHmap,state.selectedApp,state.speechResultAppName)
}
})
if (savedInstanceState==null){
viewModel.initSettingsActivityUI(appsDetailsHmap)
}
else{
viewModel.initCachedSettingsActivityUI()
}
Fragment code
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view= inflater.inflate(R.layout.fragment_added_apps, container, false)
viewModel.getCachedApplist()
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getState().observe(viewLifecycleOwner, Observer {state->
when(state){
is SettingsViewModelStates.PassAppsToFragment->{
initRecyclerView(state.list)
}
}
})
}
ViewModel
private var state=MutableLiveData<SettingsViewModelStates>()
private var appsDetailsHmap=HashMap<String,AppsDetails>()
private var addedApps= HashMap<String,Drawable?>()
fun getState()=state as LiveData<SettingsViewModelStates>
fun getCachedApplist(){
if (addedApps.isEmpty()){
getAppsList()
println("empty")
}
else
state.setValue(SettingsViewModelStates.PassAppsToFragment(addedApps))
}
fun initCachedSettingsActivityUI(){
state.setValue(SettingsViewModelStates.GetCachedData(appsDetailsHmap,selectedApp,speechResultAppName))
}
fun initSettingsActivityUI(list:HashMap<String, AppsDetails>) {
appsDetailsHmap=list
state.setValue( SettingsViewModelStates.GetAppsDetails(list))
}
states:
sealed class SettingsViewModelStates {
data class GetAppsDetails(val list:HashMap<String, AppsDetails>):SettingsViewModelStates()
data class ShowDialog(val visibility: Int) : SettingsViewModelStates()
data class PassAppsToFragment(val list:HashMap<String,Drawable?>) : SettingsViewModelStates()
data class GetCachedData(val appDetailsHmap:HashMap<String,AppsDetails>,
val selectedApp: AppsDetails,
val speechResultAppName:String ) : SettingsViewModelStates()
}