LiveData + ViewModel States , values clash and only last value is observed - android

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()
}

Related

call a fragment from another class\function kotlin

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.

How to implement a dynamic list view inside fragment android studio in Kotlin

I have two fragments that share information with each other, in the first one I have an edit text and button widget. The second fragment is just a listview. When the user clicks the button, it displays whatever is in the edit text widget in the second fragment.
So if the user enters the text study and clicks the button the second fragment will display
Study
If the user then enters the text eat and clicks the button, the second fragment will display
Study
Eat
I am having so issues with displaying the texts
So far this is what I have done
class FirstFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = activity?.run { ViewModelProvider(this)[MyViewModel::class.java]
} ?: throw Exception("Invalid Activity")
val view = inflater.inflate(R.layout.one_fragment, container, false)
val button = view.findViewById<Button>(R.id.vbutton)
val value = view.findViewById<EditText>(R.id.textView)
button.setOnClickListener {
}
return view;
}
}
class SecondFragment : Fragment() {
lateinit var viewModel: MyViewModel
#SuppressLint("MissingInflatedId")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = activity?.run { ViewModelProvider(this)[MyViewModel::class.java]
} ?: throw Exception("Invalid Activity")
val view = inflater.inflate(R.layout.page3_fragment, container, false)
val valueView = v.findViewById<TextView>(R.id.textView)
return view
The problem I am having is how to display the texts
If I undestand you correctly, you want to share data between fragments? If yes, you can do that with "shared" viewModel. For example:
class FirstFragment : Fragment() {
private var _binding: FragmentFirstBinding? = null
private val binding get() = _binding!!
private val sharedViewModel by activityViewModels<SharedViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFirstBinding.inflate(inflater, container, false)
binding.buttonChangeFragment.setOnClickListener {
/*
You can change data here, or in navigateWithNavController() from
activity (You already have an instance of your viewModel in activity)
*/
sharedViewModel.changeData(binding.myEditText.text.toString())
if (requireActivity() is YourActivity)
(requireActivity() as YourActivity).navigateWithNavController()
}
return binding.root
}
}
class SecondFragment : Fragment() {
private var _binding: FragmentSecondBinding? = null
private val binding get() = _binding!!
private val sharedViewModel by activityViewModels<SharedViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSecondBinding.inflate(inflater, container, false)
binding.secondFragmentText.text = sharedViewModel.someData.value
return binding.root
}
}
and your activity:
class YourActivity: AppCompatActivity() {
private lateinit var binding: YourActivityBinding
private lateinit var appBarConfiguration: AppBarConfiguration
private val sharedViewModel: SharedViewModel by lazy {
ViewModelProvider(
this
)[SharedViewModel::class.java]
}
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = YourActivityBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
navController = this.findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(navController.graph)
}
/*
This function is just for test
*/
fun navigateWithNavController() {
navController.navigate(R.id.secondFragment)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, appBarConfiguration)
}
}
And your viewModel should look something like this:
class SharedViewModel : ViewModel() {
private val _someData = MutableLiveData("")
val someData: LiveData<String>
get() = _someData
fun changeData(newData: String?) {
_someData.value = newData ?: _someData.value
}
}
Your view model should have a backing list of the entered words. When a word is added, the list can be updated, and in turn you can update a LiveData that publishes the latest version of the list.
class MyViewModel: ViewModel() {
private val backingEntryList = mutableListOf<String>()
private val _entryListLiveData = MutableLiveData("")
val entryListLiveData : LiveData<String> get() = _entryListLiveData
fun addEntry(word: String) {
backingEntryList += word
_entryListLiveData.value = backingEntryList.toList() // use toList() to to get a safe copy
}
}
Your way of creating the shared view model is the hard way. The easy way is by using by activityViewModels().
I also suggest using the Fragment constructor that takes a layout argument, and then setting things up in onViewCreated instead of onCreateView. It's less boilerplate code to accomplish the same thing.
In the first fragment, you can add words when the button's clicked:
class FirstFragment : Fragment(R.layout.one_fragment) {
private val viewModel by activityViewModels<MyViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val button = view.findViewById<Button>(R.id.vbutton)
val value = view.findViewById<EditText>(R.id.textView)
button.setOnClickListener {
viewModel.addEntry(value.text.toString())
}
}
}
In the second fragment, you observe the live data:
class SecondFragment : Fragment(R.layout.page3_fragment) {
private val viewModel by activityViewModels<MyViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val valueView = view.findViewById<TextView>(R.id.textView)
viewModel.entryListLiveData.observe(viewLifecycleOwner) { entryList ->
valueView.text = entryList.joinToString(" ")
}
}
}

I could not connect my recyclerview in fragment with adapter

my fragment class
class RecyclerFragment : Fragment() {
var recycler:RecyclerView? =null
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
recycler = view?.findViewById(R.id.recycler)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_recycler, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
recycler?.layoutManager = LinearLayoutManager(this.context,RecyclerView.VERTICAL,false)
recycler?.adapter = Custom_Adapter() // error-Unresolved reference: Custom_Adapter
}
}
//here is my adater
class Custom_Adapter() : RecyclerView.Adapter<Custom_Adapter.ViewHolder>() {
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): Custom_Adapter.ViewHolder {
var myViewInflater = LayoutInflater.from(viewGroup.context).inflate(R.layout.custom_layout,viewGroup,false)
return ViewHolder(myViewInflater)
}
override fun onBindViewHolder(holder: Custom_Adapter.ViewHolder, position: Int) {
}
override fun getItemCount(): Int {
return 9
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
}
Update your recycler fragment like this.
class RecyclerFragment : Fragment(R.layout.fragment_recycler) {
var recycler:RecyclerView? =null
private var param1: String? = null
private var param2: String? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recycler = view.findViewById(R.id.recycler)
recycler?.layoutManager = LinearLayoutManager(this.context,RecyclerView.VERTICAL,false)
recycler?.adapter = Custom_Adapter()
}
}
And you have to move classes Recucler_Item and Custom_Adapter into correct java package.
From your repo link in one of the comments, Custom_Adapter is under your src/test directory. It needs to be under src/main where RecyclerFragment can see it - that's why you're getting unresolved reference errors. Stick it in there with everything else
main and test are different source sets, it's basically a way of keeping stuff separate. All your production code goes in main, and it can't see anything in test by default. When you run tests, those use everything in test (and maybe androidTest) and get all the stuff in main included, since the tests need access to the production code to, y'know, test it!

How to implement databinding with Reflection in BaseFragment()

I'm trying to implement a BaseFragment in which I will pass the layout resource on it and it should outputs the binding to work in the fragment itself instead of need to do it everytime the fragment is extended.
For example I have this BaseFragment
open class BaseFragment(#LayoutRes contentLayoutId : Int = 0) : Fragment(contentLayoutId) {
private lateinit var onInteractionListener: OnFragmentInteractionListener
val toolbar : Toolbar?
get() {
return if(activity is BaseActivity)
(activity as BaseActivity).toolbar
else
null
}
override fun onAttach(context: Context) {
super.onAttach(context)
setOnInteractionListener(context)
}
...
In which I use like this
class A(): BaseFragment(R.layout.myFragment) { ... }
Now, if I use this I will need to do the definition of the binding class again in my onCreateView
class A(): BaseFragment(R.layout.myFragment) {
private lateinit var binding: MyFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.myFragment, container, false)
return binding.root
}
override fun onDestroy(){
binding = null
}
}
What I want to implement is that since I'm passwing the layout to my BaseFragment, I want my BaseFragment to handle the creation of the binding and just return me the binding in the fragment which I use to extend BaseFragment
What I want to have is something like this
class A(): BaseFragment(R.layout.myFragment) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.myTextView = ""
}
}
So my question is, how I can implement inside BaseFragment the onDestroy() and the onCreateView to always create a binding for me from the layout I'm passing in ?
I heard that I should use reflection but I'm not that sure on how to accomplish it
I didn't hear about the possibility to get the databinding just from a layout, but even if it's possible, I don't think that is the recommended way, because of two reasons:
Reflection is slow
It makes things more complicated than they are.
Instead of making magic with Reflection, you could do something like this:
abstract class BaseFragment<out VB: ViewDataBinding>(
private val layout: Int,
// Other Dependencies if wanted
) : Fragment() {
abstract val viewModel: ViewModel
// other variables that all fragments need
// This does not cause any memory leak, because you are not storing the binding property.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = DataBindingUtil.inflate<VB>(inflater, layout, container, false).apply {
lifecycleOwner = viewLifecycleOwner
setVariable(BR.viewModel, viewModel)
}.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Do some basic work here that all fragments need
// like a progressbar that all fragments share, or a button, toolbar etc.
}
And then, when you still need the bindingProperty, I would suggest the following library (it handles all the onDestoryView stuff etc):
implementation 'com.kirich1409.viewbindingpropertydelegate:viewbindingpropertydelegate:1.2.2'
You can then use this like:
class YourFragment(yourLayout: Int) : BaseFragment<YourBindingClass>() {
private val yourBinding: YourBindingClass by viewBinding()
override val viewModel: YourViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// do binding stuff
}
}
Let me know if this worked for you.
Cheers

Kotlin getting api response with foreach

So, what I'm trying to do is to get information from api and add it to model: MutableList and then set model information to recycler view. But the problem is, recycler view is being called before api gets all responses and so, my view is empty. This is what i concluded, if I'm wrong please correct me. But if not, How can I fix this problem?
class FavouritesFragment(myContext : HomeActivity, fvts: ArrayList<String>): Fragment() {
var activity = myContext
var favourites = fvts
var model : MutableList<CurrentWeatherModel> = mutableListOf()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_favourites, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
init()
}
private fun init() {
model.clear()
favourites.forEach {
getForecastModel(it)}
Log.d("other", model.size.toString())
favouritesRecyclerView.layoutManager = LinearLayoutManager(activity)
val adapter = CurrentRecyclerViewAdapter(model, activity)
favouritesRecyclerView.adapter = adapter
}
private fun getForecastModel(value: String?) {
if (!value.isNullOrEmpty()) {
DataLoader.getRequestForQuery("weather", value, "a77840cefd5a443793bd5e6b54776905", object: CustomCallback{
override fun onSuccess(result: String) {
if (result != "null") {
var converted = Gson().fromJson(result, CurrentWeatherModel::class.java)
model.add(converted)
}
}
})
}
}
}
You need to notify the adapter that data has changed, you can achieve this by calling adapter.notifyDataSetChanged() whenever the data has changed.
So you need to add this line after adding data to the list
model.add(converted)
adapter.notifyDataSetChanged()

Categories

Resources