I'm navigating from a Fragment to a DialogFragment. What I want to do is notice when this DialogFragment is dismissed in my Fragment to do something.
I'm trying doing this updating a LiveData but for some reason,when the DialogFragment is closed, the LiveData value is never true like I'm trying to do.
MyFragment:
private val myViewModel: MyViewModel by viewModel() //Using Koin
btn1.setOnClickListener{
findNavController().navigate(R.id.my_dialog_fragment)
}
myViewModel.dialogFragmentIsClosed.observe(viewLifecycleOwner){isClosed->
if(isClosed)
//do something
}
MyViewModel:
private val _dialogFragmentIsClosed = MutableLiveData(false)
val dialogFragmentIsClosed: LiveData<Boolean> get() = _dialogFragmentIsClosed
fun isDialogFragmentClosed(closed:Boolean){
_dialogFragmentIsClosed.postValue(closed)
}
DialogFragment:
private val myViewModel: MyViewModel by viewModel() //Using Koin
override fun onDismiss(dialog:DialogInterface){
myViewModel.isDialogFragmentClosed(true)
val bundle = bundleOf(Pair("argBoolean",true))
findNavController().navigate(R.id.my_fragment,bundle)
}
First of all, your DialogFragment and MyFragment are not sharing the same instance of MyViewModel.
To achieve your goal, you should check this document:
https://developer.android.com/guide/navigation/navigation-programmatic#additional_considerations
You will open DialogFragment from MyFragment, then observer the result from DialogFragment.
If you want to share the same instance of ViewModel, change to use activityViewModel() ( instead of using viewModel() )
You could use a interface to listen dismiss event. This is a simple way
First, create interface and put it in your dialog fragment:
interface CloseListener {
fun onClose()
}
class YourDialog(private val listener: CloseListener) : DialogFragment() {
override fun onDismiss(dialog:DialogInterface){
listener.onClose()
}
}
And then, in your fragment, calling dialogfragment like this:
val yourDialog = YourDialog(object: CloseListener{
override fun onClose() {
//do something here, such as set value for your viewModel
}
})
yourDialog.show(childFragmentManager, null)
Hope it can help you
Related
My app uses MVVM architecture. I have a ViewModel shared by both an Activity and one of its child fragments. The ViewModel contains a simple string that I want to update from the Activity and observe in the fragment.
My issue is simple: the observe callback is never reached in my fragment after the LiveData updates. For testing, I tried observing the data in MainActivity, but that works fine. Additionally, observing LiveData variables in my fragment declared in other ViewModels works fine too. Only this ViewModel's LiveData seems to pose a problem for my fragment, strangely.
I'm declaring the ViewModel and injecting it into my Activity and Fragment via Koin. What am I doing incorrectly to never get updates in my fragment for this ViewModel's data?
ViewModel
class RFIDTagViewModel: ViewModel() {
private val _rfidTagUUID = MutableLiveData<String>()
val rfidTagUUID: LiveData<String> = _rfidTagUUID
fun tagUUIDScanned(tagUUID: String) {
_rfidTagUUID.postValue(tagUUID)
}
}
Activity
class MainActivity : AppCompatActivity(), Readers.RFIDReaderEventHandler,
RFIDSledEventHandler.TagScanInterface {
private val rfidViewModel: RFIDTagViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
rfidViewModel.rfidTagUUID.observe(this, {
Timber.d("I'm ALWAYS reached")
})
}
override fun onResume() {
rfidViewModel.tagUUIDScanned(uuid) //TODO: data passed in here, never makes it to Fragment observer, only observed by Activity successfully
}
}
Fragment
class PickingItemFragment : Fragment() {
private val rfidViewModel: RFIDTagViewModel by viewModel()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
rfidViewModel.rfidTagUUID.observe(viewLifecycleOwner, { tagUUID ->
Timber.d("I'm NEVER reached")
})
}}
Koin DI Config
val appModule = module {
viewModel { RFIDTagViewModel() }
}
In your Fragment I see you are using viewModels(). viewModels() here will be attached to the Fragment, not to the Activity.
If you want to shareViewModel between Fragment and Activity, then in Fragment you use activityViewModels(). Now, in the Fragment, your shareViewModel will be attached to the Activity containing your Fragment.
Edit as follows:
PickingItemFragment.kt
class PickingItemFragment : Fragment() {
private val rfidViewModel: RFIDTagViewModel by activityViewModels()
}
More information: Communicating with fragments
You need to use the same viewmodel, aka, sharedViewModel, the way you are doing you are using two different instances of the same viewmodel.
To fix it.
On both activity and fragment:
private val rfidViewModel: RFIDTagViewModel by activityViewModels()
https://developer.android.com/topic/libraries/architecture/viewmodel?hl=pt-br
I have an app with one activity and two fragments, in the first fragment, I should be able to insert data to the database, in the second I should be able to see the added items in a recyclerView.
So I've made the Database, my RecyclerView Adapter, and the ViewModel,
the issue is now how should I manage all that?
Should I initialize the ViewModel in the activity and call it in some way from the fragment to use the insert?
Should I initialize the viewmodel twice in both fragments?
My code looks like this:
Let's assume i initialize the viewholder in my Activity:
class MainActivity : AppCompatActivity() {
private val articoliViewModel: ArticoliViewModel by viewModels {
ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
}
Then my FirstFragments method where i should add the data to database using the viewModel looks like this:
class FirstFragment : Fragment() {
private val articoliViewModel: ArticoliViewModel by activityViewModels()
private fun addArticolo(barcode: String, qta: Int) { // function which add should add items on click
// here i should be able to do something like this
articoliViewModel.insert(Articolo(barcode, qta))
}
}
And my SecondFragment
class SecondFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
private val articoliViewModel: ArticoliViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.recyclerView)
val adapter = ArticoliListAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(activity)
// HERE I SHOULD BE ABLE DO THIS
articoliViewModel.allWords.observe(viewLifecycleOwner) { articolo->
articolo.let { adapter.submitList(it) }
}
}
}
EDIT:
My ViewModel looks like this:
class ArticoliViewModel(private val repository: ArticoliRepository): ViewModel() {
val articoli: LiveData<List<Articolo>> = repository.articoli.asLiveData()
fun insert(articolo: Articolo) = viewModelScope.launch {
repository.insert(articolo)
}
}
class ArticoliViewModelFactory(private val repository: ArticoliRepository): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ArticoliViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return ArticoliViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Whether multiple fragments should share a ViewModel depends on whether they are showing the same data. If they show the same data, I think it usually makes sense to share a ViewModel so the data doesn't have to be pulled from the repository when you switch between them, so the transition is faster. If either of them also has significant amount of unique data, you might consider breaking that out into a separate ViewModel so it doesn't take up memory when it doesn't need to.
Assuming you are using a shared ViewModel, you can do it one of at least two different ways, depending on what code style you prefer. There's kind of a minor trade-off between encapsulation and code duplication, although it's not really encapsulated anyway since they are looking at the same instance. So personally, I prefer the second way of doing it.
Each ViewModel directly creates the ViewModel. If you use by activityViewModels(), then the ViewModel will be scoped to the Activity, so they will both receive the same instance. But since your ViewModel requires a custom factory, you have to specify it in both Fragments, so there is a little bit of code duplication:
// In each Fragment:
private val articoliViewModel: ArticoliViewModel by activityViewModels {
ArticoliViewModelFactory((application as ArticoliApplication).repository)
}
Specify the ViewModel once in the MainActivity and access it in the Fragments by casting the activity.
// In Activity: The same view model code you already showed in your Activity, but not private
// In Fragments:
private val articoliViewModel: ArticoliViewModel
get() = (activity as MainActivity).articoliViewModel
Or to avoid code duplication, you can create an extension property for your Fragments so they don't have to have this code duplication:
val Fragment.articoliViewModel: ArticoliViewModel
get() = (activity as MainActivity).articoliViewModel
I have a BaseFragment.kt that looks like this
open class BaseFragment: Fragment() {
private lateinit var viewModel: BaseViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(BaseViewModel::class.java)
observeNavigationCommands()
}
/**
* Method that observes Navigation commands triggered by BaseViewHolder
* This allows us to navigate from a viewHolder using the MVVM pattern
*/
private fun observeNavigationCommands() {
viewModel.navigationCommands.observe(viewLifecycleOwner, EventObserver {
Timber.e("received nav command $it")
when(it) {
is NavigationCommand.To -> findNavController().navigate(it.destinationId)
is NavigationCommand.Back -> findNavController().popBackStack()
is NavigationCommand.BackTo -> TODO()
NavigationCommand.ToRoot -> TODO()
}
})
}
}
...and a BaseViewModel.kt that looks like this
open class BaseViewModel: ViewModel() {
val navigationCommands = MutableLiveData<Event<NavigationCommand>>()
/**
* Navigate to a specific fragment using Id
*/
fun navigate(id: Int) {
Timber.e("trigger navigation event $id")
// navigationCommands.postValue(NavigationCommand.To(id))
navigationCommands.value = Event(NavigationCommand.To(id))
}
/**
* Pop backStack
*/
fun goBack() {
navigationCommands.value = Event(NavigationCommand.Back)
}
}
the NavigationCommand class looks like
sealed class NavigationCommand {
data class To(val destinationId: Int) : NavigationCommand()
data class BackTo(val destinationId: Int): NavigationCommand()
object Back: NavigationCommand()
object ToRoot: NavigationCommand()
}
Now in my other viewModels that extend BaseViewModel I want to be able to call
navigate(R.id.action_fragmentA_to_fragmentB) but the issue is that the consumer in observeNavigationCommands() never receives the NavigationCommands
.....
But if I copy the content of observeNavigationCommands() and place it in my current fragment (the one that extends BaseFragment) the consumer receives the updates
what am I missing? Please help
Do I understand correctly that for your fragment that extends BaseFragment, you want to attach a viewModel that extends BaseViewModel, but that doesn't work for you with the liveData?
If so, check out this simple working project that I created to mirror you case:
https://github.com/phamtdat/OpenViewModelDemo
The point is to make the viewModel overridedable and override it in your fragment that extends BaseFragment.
I wondered if it's possible to pass a String data which has declared in Activity class and pass the String data to ViewModel class then pass the data to Fragment class.
ViewModel Class
class TimeTableViewModel extends ViewModel {
private MutableLiveData<String> start_time_str = new MutableLiveData<>();
void send_StartTime(String start_Time){
start_time_str.setValue(start_Time);
}
LiveData<String> get_StartTime(){
return start_time_str;
}}
In ViewModel Class, I have MutableLiveData<String> start_time_str and it has been initialized as new MutableLiveData<>();
I would like to use void send_StartTime(String start_Time) function in Activity class to set value of argument String start_Time and call the start_time_str in Fragment class.
Activity Class
#Override
public boolean onOptionsItemSelected(MenuItem item){
switch (item.getItemId()){
case android.R.id.home:
finish();
break;
case R.id.add_schedule_save:
String start_time_str = startTime.getText().toString();
Intent intent_restart0 = new Intent(TimeTable_Add_New_Schedule.this, MainActivity.class);
startActivity(intent_restart0);
TimeTableViewModel timeTableViewModel = new TimeTableViewModel();
timeTableViewModel.send_StartTime(start_time_str);
Toast.makeText(this,""+start_time_str,Toast.LENGTH_LONG).show();
break;
}
return super.onOptionsItemSelected(item);
}
Fragment Class
#Override
public void onActivityCreated(#Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
TimeTableViewModel timeTableViewModel = new TimeTableViewModel();
timeTableViewModel.get_StartTime().observe(getViewLifecycleOwner(), new Observer<String>() {
#Override
public void onChanged(String s) {
mon_textView_11.setText(s);
}
});
}
In the Fragment class I call get_StartTime() function to get start_time_str and set the String value to my TextView. I think the start_time_str has been successfully set by function of timeTableViewModel.send_StartTime(start_time_str); in the Activity Class because of Toast.maketext is worked like a charm. However the TextView is not shown anything. I have tested Text Color is not white so that if the string value is correctly called, it should be appear on screen. If you have any suggestions, I would love to hear your advice.
Thank you very much.
It really depends on how do you create your ViewModel instance. Now you are creating ViewModel by its constructor, but that is not a proper way. You should use ViewModelProvider or extension methods that were created by Google team.
If you go with ViewModelProvider you should do it like this:
TimeTableViewModel viewModel = new ViewModelProvider(this).get(TimeTableViewModel.class);
It is important to pass the correct context to ViewModelProvider constructor call. If you are in fragment and you will just use getContext() instead of getActivity(), you will not get the same instance as it was created in Activity. You will create a new instance of ViewModel, that will be scoped only inside of fragment lifecycle. So it is important to use in both parts activity context to get the same instance.
Activity part:
TimeTableViewModel viewModel = new ViewModelProvider(this).get(TimeTableViewModel.class);
Fragment part:
TimeTableViewModel viewModel = new ViewModelProvider(getActivity()).get(TimeTableViewModel.class);
Is important that your fragment is located inside the same activity that is using this ViewModel.
But guys at Google has make it easier for us with some extension methods. But as far as I know, they are working only in Kotlin classes. So if you have Kotlin code, you can declare your ViewModel simply like this:
private val quizViewModel: TimeTableViewModel by activityViewModels()
For Fragment scoped ViewModel you need to write something like this:
private val quizViewModel: TimeTableViewModel by viewModels()
But you have to add Kotlin ktx dependency to your project build.gradle file. For example like this:
implementation 'androidx.fragment:fragment-ktx:1.1.0'
If you are using Android Architecture and want to share activityViewModel in your fragments.
To get viewModels in fragment use below code:
private val fragmentViewModel: Fragment1ViewModel by viewModels()
private val activityViewModel: MainActivityViewModel by activityViewModels()
and in MainActivity use below code:
private val activityViewModel: MainActivityViewModel by viewModels()
I'm using kotlin language
I'm used same as tutorial but not working
Here's example codes how to declare my ViewModel
add dependencies in build.gradle:
implementation 'androidx.fragment:fragment-ktx:1.4.1'
example of Activity class
class HomepageActivity : AppCompatActivity() {
private lateinit var sharedViewModel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityHomepageBinding.inflate(layoutInflater)
setContentView(binding.root)
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
sharedViewModel.isMenuOpen.observe(this, {
onMenuOpen(it)
})
}
example in Fragment class
class HomeFragment : Fragment() {
private lateinit var sharedViewModel: SharedViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel = activity!!.run{
ViewModelProvider(this).get(SharedViewModel::class.java)
}
}
I found the simplest fix to this problem, instead of giving the owner as "this" change it to getActivity().
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (mvm == null) {
mvm = new ViewModelProvider(getActivity()).get(CounterFgViewModel.class);
}
}
I've created a structure in app with BaseActivity and BaseViewModel. All other activities/viewModels must be extend with this base classes. I made that cause i need to call some methods in any activity (like showInfo() method).
When i update LiveData in BaseViewModel and observe it in BaseActivity all works well. But when i update that LiveData in child ViewModel (e.g. UsersViewModel) only with BaseActivity observing its won't work.
What should i do when i want to call some base method in any activity through ViewModel?
open class BaseActivity : AppCompatActivity() {
//inject viewModel with Koin
private val baseViewModel: BaseViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
baseViewModel.actionShowInfo.observe(this, Observer {
showInfo(it)
}
}
protected fun showInfo(message: String) {
AlertDialog.Builder(this)
.setMessage(message)
.setPositiveButton(R.string.ok, null)
.show()
}
}
open class BaseViewModel : ViewModel() {
private val actionShowInfo = MutableLiveData<String>()
init {
actionShowInfo.postValue("some base info") //showInfo() in BaseActivity will be called
}
}
class UsersActivity : BaseActivity() {
private val usersViewModel: UsersViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(
}
}
class UsersViewModel: BaseViewModel {
init {
//showInfo() in BaseActivity will not be called
actionShowInfo.postValue("some info")
}
}
Just by extend the UserViewModel your BaseViewModel, doesn't mean it's sharing the same instance. Based on your requirement, I think you need a ViewModel that can share it's instance to several activity, so that when you update the ViewModel on Activity A, you can observe the change on Activiy B, and so on.
This is where SharedViewModel come to rescue. You need to implement a sharedViewModel to all your activity.
private val baseViewModel: BaseViewModel by sharedViewModel()
Reference: https://doc.insert-koin.io/#/koin-android/viewmodel?id=shared-viewmodel