I want to navigate to an other Fragment when the user selcts a value (player id) in the AlertDialog. I use this change to the id to trigger the navigation. Unfortunately the observer of this property doesn't get triggerd.
Please see the following code:
SelectCharacterDialogFragment.kt
#AndroidEntryPoint
class SelectCharacterDialogFragment : DialogFragment() {
val viewModel: WelcomeViewModel by viewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
super.onCreateDialog(savedInstanceState)
val names = arguments?.getStringArrayList("names")
val ids = arguments?.getIntegerArrayList("ids")
val dialogBuilder = MaterialAlertDialogBuilder(requireContext())
.setTitle("Choose a character")
.setItems(names!!.toTypedArray()) { dialog, which ->
val playerId = ids?.get(which)
Timber.d("Selected player id: %d", playerId)
viewModel.setActivePlayerId(playerId!!)
}
return dialogBuilder.create()
}
}
WelcomeViewModel.kt
#HiltViewModel
class WelcomeViewModel #Inject constructor(private val repository: PlayerRepository) : ViewModel() {
private val _activePlayerId = MutableLiveData<Int>()
val playerId:LiveData<Int> =_activePlayerId
fun setActivePlayerId(playerId: Int){
setActivePlayer(playerId)
_activePlayerId.value =playerId
}
private fun setActivePlayer(playerId: Int) {
viewModelScope.launch {
repository.setActivePlayer(playerId)
Timber.d("Active player set to id: %d", playerId)
}
}
}
WelcomeFragment.kt
#AndroidEntryPoint
class WelcomeFragment : Fragment() {
private val viewModel: WelcomeViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewModel.playerId.observe(viewLifecycleOwner){
Timber.d("Observing active player id: %d", it)
findNavController().navigate(
WelcomeFragmentDirections.actionWelcomeFragmentToNavigationPlayer()
)
}
}
}
The last log output I see is from the method WelcomeViewModel#setActivePlayer(playerId: Int). Maybe it's obvious but I just can't figure out what's wrong with the code.
You've to use shared viewmodel.
Update your fragment and dialog as well
val viewModel: WelcomeViewModel by activityViewModels()
Related
i have a really simple vocabulary note app contains 2 fragment and 1 root activity. In HomeFragment i have a button "addVocabularyButton". When it is clicked a BottomSheetDialogFragment appears and user gives 3 inputs and with a viewmodel it is saved in DB. My problem is when i save the input to the DB it works fine but i cannot see in HomeFragment that word instantaneously. I have to re-run the app to see in home fragment. I am using Navigation library and recycler view in home fragment.
Github link : https://github.com/ugursnr/MyVocabularyNotebook
Home Fragment
class HomeFragment : Fragment() {
private var _binding : FragmentHomeBinding? = null
private val binding get() = _binding!!
private var vocabularyAdapter = VocabulariesHomeAdapter()
private lateinit var sharedViewModel: AddVocabularySharedViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(layoutInflater,container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//sharedViewModel = ViewModelProvider(this)[AddVocabularySharedViewModel::class.java]
sharedViewModel = (activity as MainActivity).sharedViewModel
sharedViewModel.getAllVocabulariesFromDB()
observeAllVocabularies()
prepareRecyclerView()
addVocabularyOnClick()
vocabularyAdapter.onItemDeleteClicked = {
sharedViewModel.deleteVocabulary(it)
observeAllVocabularies()
}
}
private fun prepareRecyclerView(){
binding.recyclerViewHome.apply {
layoutManager = LinearLayoutManager(context)
adapter = vocabularyAdapter
}
}
private fun addVocabularyOnClick(){
binding.addVocabularyButton.setOnClickListener{
val action = HomeFragmentDirections.actionHomeFragmentToAddVocabularyBottomSheetFragment()
Navigation.findNavController(it).navigate(action)
}
}
private fun observeAllVocabularies(){
sharedViewModel.allVocabulariesLiveData.observe(viewLifecycleOwner, Observer {
vocabularyAdapter.updateVocabularyList(it)
})
}
}
Dialog Fragment
class AddVocabularyBottomSheetFragment : BottomSheetDialogFragment() {
private var _binding : FragmentAddVocabularyBottomSheetBinding? = null
private val binding get() = _binding!!
private lateinit var sharedViewModel: AddVocabularySharedViewModel
private var vocabularyInput : String? = null
private var translationInput : String? = null
private var sampleSentenceInput : String? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAddVocabularyBottomSheetBinding.inflate(layoutInflater,container,false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//sharedViewModel = ViewModelProvider(this)[AddVocabularySharedViewModel::class.java]
sharedViewModel = (activity as MainActivity).sharedViewModel
binding.addOrUpdateVocabularyButton.setOnClickListener {
vocabularyInput = binding.vocabularyActualET.text.toString()
translationInput = binding.vocabularyTranslationET.text.toString()
sampleSentenceInput = binding.vocabularySampleSentenceET.text.toString()
val inputVocabulary = Vocabulary(vocabularyInput,translationInput,sampleSentenceInput)
insertVocabularyToDB(inputVocabulary)
sharedViewModel.getAllVocabulariesFromDB()
dismiss()
}
}
private fun insertVocabularyToDB(vocabulary: Vocabulary){
sharedViewModel.insertVocabulary(vocabulary)
}
}
Shared ViewModel
class AddVocabularySharedViewModel(application: Application) : AndroidViewModel(application) {
private var _allVocabulariesLiveData = MutableLiveData<List<Vocabulary>>()
private var _vocabularyLiveData = MutableLiveData<Vocabulary>()
val allVocabulariesLiveData get() = _allVocabulariesLiveData
val vocabularyLiveData get() = _vocabularyLiveData
val dao = VocabularyDatabase.makeDatabase(application).vocabularyDao()
val repository = VocabularyRepository(dao)
fun insertVocabulary(vocabulary: Vocabulary) = CoroutineScope(Dispatchers.IO).launch {
repository.insertVocabulary(vocabulary)
}
fun updateVocabulary(vocabulary: Vocabulary) = CoroutineScope(Dispatchers.IO).launch {
repository.updateVocabulary(vocabulary)
}
fun deleteVocabulary(vocabulary: Vocabulary) = CoroutineScope(Dispatchers.IO).launch {
repository.deleteVocabulary(vocabulary)
}
fun getAllVocabulariesFromDB() = CoroutineScope(Dispatchers.IO).launch {
val temp = repository.getAllVocabulariesFromDB()
withContext(Dispatchers.Main){
_allVocabulariesLiveData.value = temp
}
}
fun getVocabularyDetailsByID(vocabularyID : Int) = CoroutineScope(Dispatchers.IO).launch {
val temp = repository.getVocabularyDetailsByID(vocabularyID).first()
withContext(Dispatchers.Main){
_vocabularyLiveData.value = temp
}
}
}
Adapter
class VocabulariesHomeAdapter : RecyclerView.Adapter<VocabulariesHomeAdapter.VocabulariesHomeViewHolder>() {
lateinit var onItemDeleteClicked : ((Vocabulary) -> Unit)
val allVocabulariesList = arrayListOf<Vocabulary>()
class VocabulariesHomeViewHolder(val binding : RecyclerRowBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VocabulariesHomeViewHolder {
return VocabulariesHomeViewHolder(RecyclerRowBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: VocabulariesHomeViewHolder, position: Int) {
val vocabulary = allVocabulariesList[position]
holder.binding.apply {
actualWordTV.text = vocabulary.vocabulary
translationWordTV.text = vocabulary.vocabularyTranslation
deleteButtonRV.setOnClickListener {
onItemDeleteClicked.invoke(vocabulary)
notifyItemRemoved(position)
}
}
}
override fun getItemCount(): Int {
return allVocabulariesList.size
}
fun updateVocabularyList(newList : List<Vocabulary>){
allVocabulariesList.clear()
allVocabulariesList.addAll(newList)
notifyDataSetChanged()
}
}
I know there are lots of codes up there but i have a really big problems about using these dialog fragments. Thank you for your help.
This is because multiple instances of the same View Model are created by the Navigation Library for each Navigation Screen.
You need to tell the Navigation Library to share the same ViewModel between all navigation screens.
Easiest way to fix this is to scope the viewModel to the Activity rather than a Fragment and using it in all your fragments.
val viewModel = ViewModelProvider(requireActivity()).get(MyViewModel::class.java)
This way, the viewModel is scoped to the Application instance rather than Fragment. This will keep the state in the viewModel persistent across the Application.
You can also do this by scoping the viewModel to the navigation graph.
val myViewModel: MyViewModel by navGraphViewModels(R.id.your_nested_nav_id)
Alternate method, if you're using dependency injection libraries
val navController = findNavController();
val navBackStackEntry = navController.currentBackStackEntry!!
If you use hilt, you can just pass your NavBackStackEntry of the NavGraph to hiltViewModel()
val viewModel = hiltViewModel<MyViewModel>(//pass NavBackStackEntry)
This will give you a viewModel that is scoped to NavBackStackEntry and will only be recreated when you pop the NavBackStackEntry(ie Navigate out of the navigation screens.)
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(" ")
}
}
}
My app should be able to change color of one part of my screen in a Fragment (called Color Preview) from Color Picker that is DialogFragment and appears once a Button is clicked. I used the same ViewModel with MutableLiveData in it within Fragment and DialogFragment. However I am not able to get Data about color in Fragment when I pick some color in DialogFragment. Code below.
Dialog Fragment:
class ColorFragment : DialogFragment() {
private var _binding: FragmentColorBinding? = null
private val binding get() = _binding!!
private val viewModel: MainViewModel by lazy {
getViewModel {
MainViewModel()
}
}
#SuppressLint("DialogFragmentInsteadOfSimpleDialog")
override fun onStart() {
super.onStart()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
FragmentColorBinding.inflate(inflater, container, false).apply {
_binding = this
lifecycleOwner = this#ColorFragment
mainViewModel = getViewModel()
return root
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.color02.setOnClickListener {
viewModel.currentLightProfileColor.value = "#00ffff"
}
}
Main Fragment (where color should be shown) looks like this:
class MainFragment : Fragment() {
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
private var viewCreated = false
private val viewModel: MainViewModel by lazy {
getViewModel {
MainViewModel()
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
FragmentMainBinding.inflate(inflater, container, false).apply {
_binding = this
lifecycleOwner = this#MainFragment
mainViewModel = getViewModel()
executePendingBindings()
viewCreated = true
return root
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.colorPreview.holder?.addCallback(this)
viewModel.currentLightProfileColor.observe(viewLifecycleOwner) { color ->
Color.parseColor(color.toString()).let { color ->
Timber.d("color LiveDataGet: ${color}")
binding.colorPreview.setBackgroundColor(color)
}
}
binding.color.setOnClickListener {
findNavController().navigate(R.id.ColorFragment)
}
}
In my ViewModel there is only MutableLiveData for color:
class MainViewModel : ViewModel() {
val currentLightProfileColor = MutableLiveData<String>()}
I dont get any errors but once I click on color2 I should get from Timber in Log: "color LiveDataGet: #00ffff" but I dont and color preview does not change color.
Did I miss something?
I would appreciate if someone could quickly take a look at my code. Thanks!
I found a solution. In Kotlin, data is not shared if viewModels() is instantiated without activityViewModels().
So I changed my code, instead of:
private val viewModel: MainViewModel by lazy {
getViewModel {
LightmvpViewModel()
}
}
I wrote:
private val viewModel: MainViewModel by activityViewModels()
With that simple change, everything should work.
I have a parent fragment which fetches a list from API using ViewModel and Retrofit, the ViewModel is injected with Hilt.
After the list gets fetched the parent fragment will pass to its child fragment that is inside of parent fragment.
but the problem is that ViewModel is instantiated one more time in the child fragment.
Parent Fragment
#AndroidEntryPoint
class ParentFragment : Fragment() {
override val mViewModel: URLViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mViewBinding = getViewBinding(inflater, container)
mViewModel.liveData.observe(this, { data ->
{
childFragmentManager.beginTransaction().apply {
replace(
mViewBinding.fragmentContainer.id,
ChildFragment(data)
)
}
commit()
} })
mViewModel.getURL("TEST", "2021-06-18", "2021-07-18", 1 , 0 , -1, false)
return mViewBinding.root
}
}
ChildFragment
#AndroidEntryPoint
class ChildFragment(val data: List<Item>) : Fragment() {
override val mViewModel: URLViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mViewBinding = getViewBinding(inflater, container)
// mViewModel is instantiated again and some all strings properties of it is null.
return mViewBinding.root
}
}
URLViewModel
#HiltViewModel
class URLViewModel #Inject constructor(private val urlApi: URLApi): ViewModel() {
private val _urlLiveData = MutableLiveData<State<Any?>>()
val urlLiveData: LiveData<State<Any?>> = _urlLiveData
var urlName: String? = null
var beginDate: String? = null
var endDate: String? = null
var adultCount = 0
var childrenCount = 0
var airportId = 0
var isRoundTrip = false
init {
Log.e("URLViewModel", "iniialed again" )
}
#ExperimentalStdlibApi
fun getUrl(urlName: String, beginDate: String, endDate: String, adultCount: Int, childCount: Int, airportId: Int, isRoundTrip: Boolean){
Log.e("XXXXXX", "getUrl: called with url of " + urlName )
this.urlName = urlName
this.beginDate = beginDate
this.endDate = endDate
this.adultCount = adultCount
this.childrenCount = childCount
this.airportId = airportId
this.isRoundTrip = isRoundTrip
val mutableLiveData = MutableLiveData<State<Any?>>()
mutableLiveData.value = State.loading()
viewModelScope.launch {
val res = urlApi.getURL(urlName,beginDate,endDate,adultCount,childCount,airportId,isRoundTrip)
Log.e("URLVIewModel", "getUrl: response received" )
_urlLiveData.value = res
}
}
}
when I wanna access some properties like beginDate, they are null, because the ViewModel is instantiated again,
viewModels() delegation create view model against the same instance i.e Fragment's instance in your case. What you need to do is to create a shared View model .
There is helper delegate available for it with ktx libraries.
add the ktx dependency which you already have i guess from here.
implementation "androidx.fragment:fragment-ktx:1.3.4"
And create view model with
private val viewModel by activityViewModels<UrlViewModel>()
You do not have to use activity shared view model. Simply request view model from parent fragment in ChildFragment.
private val viewModel by viewModels<UrlViewModel>(ownerProducer = { requireParentFragment() })
You are trying to use share viewmodel. Try to following code for reference.
#AndroidEntryPoint
class ParentFragment : Fragment() {
private lateinit var viewModel: URLViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(requireActivity()).get(URLViewModel::class.java)
}
}
#AndroidEntryPoint
class ChildFragment : Fragment() {
private lateinit var viewModel: URLViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(requireActivity()).get(URLViewModel::class.java)
}
}
I am using a fragment and viewmodel where the list values are not updated in the viemodel when the function is called from my fragment.
This is in my fragment
class SettingsFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = FragmentSettingsBinding.inflate(inflater)
val application = requireNotNull(this.activity).application
val dataSource = UserInfoDatabase.getInstance(application).userInfoDao
val viewModelFactory = SettingsViewModelFactory(dataSource, application)
val settingsViewModel = ViewModelProvider(this, viewModelFactory).get(SettingsViewModel::class.java)
binding.settingsViewModel = settingsViewModel
binding.lifecycleOwner = this
settingsViewModel.addValueToList("hello")
}
}
This is from my ViewModel
class SettingsViewModel(val database: UserInfoDatabaseDao, application: Application): AndroidViewModel(application){
private val _sectionList = MutableLiveData<MutableList<String>>()
val sectionList: MutableLiveData<MutableList<String>>
get() = _sectionList
fun addValueToList(listValue: String){
_sectionList.value?.add(listValue)
}
}
When I try to print a toast message as _sectionList.value.toString() from my viewModel after trying to add values to the list, it shows null.
Thanks in advance for the help..
It's because you have not initialized the MutableList<>().
class SettingsViewModel(val database: UserInfoDatabaseDao, application: Application): AndroidViewModel(application){
private val _sectionList = MutableLiveData<MutableList<String>>()
val sectionList: MutableLiveData<MutableList<String>>
get() = _sectionList
fun addValueToList(listValue: String){
// In the following line: _sectionList.value is still null, so this will
// never call the add
_sectionList.value?.add(listValue)
}
}
If you initialize with:
init {
_sectionList.value = mutableListOf<String>()
}
it should work hopefully.