I try to preserve viewModel data against activity re-creation , but on onCreate() method from Fragment I got the following error : java.lang.IllegalArgumentException: SavedStateProvider with the given key is already registered
Even if the viewModel has never been create before, it causes a crash on the first app run
This is the relevent code:
//DI
viewModel { (param: String) -> MyViewModel(param, get(), get(), get()) }
//Fragment
private lateinit var viewModel: MyViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
viewModel = getStateViewModel() { parametersOf(args.param)
}
//VM
class MyViewModel: ViewModel()(
private val param: String,
private val savedState: SavedStateHandle,
private val service1: Service1,
private val service2: Service2
)
Any help?
Related
I am learning mvvm. I want to pass frgment bundle argument in ViewModel savedstatehandle .I am using Hilt-Dagger. Please help me. Everytime savedstatehandle is null.
here is my code.
FIRST FRAGMENT:
#AndroidEntryPoint
class CustomerOrderSubmit : Fragment() {
private lateinit var binding: FragmentCustomerOrderSubmitBinding
val customerViewModel: CustomerViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentCustomerOrderSubmitBinding.inflate(inflater, container, false)
binding.customerModel = customerViewModel
binding.lifecycleOwner = viewLifecycleOwner
customerViewModel.btnClick.observe(viewLifecycleOwner) {
when (it) {
true -> {
val args = Bundle()
args.putParcelable("customer",customerViewModel.args.value)
val orderReview = OrderReview()
orderReview.arguments = args
Log.d("PARC",args.toString())
val fragmentManager = requireActivity().supportFragmentManager.beginTransaction()
fragmentManager.replace(R.id.main_frame_layout,orderReview)
.addToBackStack(null).commit()
}
else -> {}
}
}
SECOND FRAGMENT: HERE I AM GETTING BUNDLE VALUE. BUT I WANT IT IN ViewModel
#AndroidEntryPoint
class OrderReview : Fragment() {
private lateinit var binding: FragmentOrderReviewBinding
private val customerViewModel: CustomerViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
binding = FragmentOrderReviewBinding.inflate(inflater, container, false)
binding.argsModel = customerViewModel
binding.lifecycleOwner = viewLifecycleOwner
val args : CustomerModel? = arguments?.getParcelable("customer")
Log.d("PARCF",args.toString())
return binding.root
}
}
VIEW MODEL: But getArgs.value is always null.
#HiltViewModel
class CustomerViewModel #Inject constructor(
private val cusomerRepository: CusomerRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val customerList: LiveData<List<CustomerModel>>
//get arguments
val getArgs = MutableLiveData<CustomerModel>(savedStateHandle["customer"])
I am getting Bundle data from fragments one to two but can't get the same data from Fragment one to ViewmodelSavedstatehandle but null every time. What have I done wrong? I need data in ViewModel.
Please help I am a new learner in MVVM.
I don't think you can pass data in this way from fragment to ViewModel.Pass this bundle data by accessing the method where it is needed.
ViewModel method
fun aMethodName(customr:String){
//your code here
//access customer
//Data type String can be changed to customer DataType
}
Fragment
from onCreateView or onViewCreated you can send the data to viewmodel
customerViewModel.aMethodName(customer)
I have an app with a single activity but with many Fragments. I am using ViewModel for my Activity-Fragment communication. Lately, I am using Hilt, and I am having a problem now communicating between my activity and fragments.
My Viewmodel
#HiltViewModel
class AppViewModel #Inject internal constructor(
): ViewModel() {
private var _data = MutableLiveData<String>()
val data: LiveData<String>
get() = _data
fun insertData(dataStr: String) {
_data.value = dataStr
}
}
My MainActivity
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mViewModel: AppViewModel by viewModels()
private var dataString: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mViewModel.data.observe(this, {
dataString = it
})
}
}
One of my Fragments
#AndroidEntryPoint
class ReportFragment : Fragment() {
private val reportViewModel: ReportViewModel by viewModels()
private val appViewModel: AppViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
...
appViewModel.insertData("Hello")
...
}
}
When I run the app, I am getting null as a result of data. Any solution to solve this?
Not sure if this is the exact issue, but you get the ViewModel inside your fragment using by activityViewModels<AppViewModel> and not by viewModels
EDIT:
Also, I just noticed you are using an internal constructor. Try using only inject constructor once and let me know if it fixed it for you :)
I have a tablayout inside a fragment. The tab layout has 3 tabs. Which tab has a fragment.
And, in the first fragment I insert a string and add it to a viewModel MutableLiveData<ArrayList>> variable.
Then, I want to observe in the third fragment.
My main ViewModel:
class MainViewModel : ViewModel() {
val message = MutableLiveData<ArrayList<String>>(arrayListOf())
fun myMessage(msg: String) {
message.value?.add(msg)
}
}
Third Fragment:
class Fragment3 : Fragment() {
lateinit var model: MainViewModel
private lateinit var viewModel: MainViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view= inflater.inflate(R.layout.fragment3_fragment, container, false)
model = activity?.let { ViewModelProvider(it).get(MainViewModel::class.java) }!!
model.message.observe(viewLifecycleOwner, Observer {
Log.e("aqui", "aqui$it")
})
return view
}
}
My first Fragment:
class Fragment1 : Fragment() {
lateinit var model: MainViewModel
private lateinit var viewModel: MainViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view= inflater.inflate(R.layout.fragment1_fragment, container, false)
model = activity?.let { ViewModelProvider(it).get(MainViewModel::class.java) }!!
var btnTest= view.findViewById<Button>(R.id.btn_test)
btnTest.setOnClickListener {
model.myMessage(et_text.text.toString())
}
return view
}
}
When I open the Third fragment it observes as planned. When I come back to first Fragment and add more Strings to the array list, the third does not observes again, I don't know why.
First Observer:
My layout:
My observer only observes once.
You have to change the value of the live data instead of just adding an item to the already set list.
Change your function myMessage as:
fun myMessage(msg: String) {
val list = message.value
list.add(msg)
message.value = list
}
This will take your existing list then add your new item and then set the value of the live data so that the observe will be called.
I wanna make a BaseFragment. For this, I have to use ViewDataBinding and ViewModel. using generic, I can use variable but not static field. For example I have to Inflate writing this code "FragmentSecondBinding.inflate(layoutInflater, container, false) ". So I tried this code "T.inflate(layoutInflater, container, false)" but got some error. Also ViewModel is like this.
How can I make this code to BaseCode?
abstract class BaseFragment<T: ViewDataBinding, M : ViewModel> : DaggerFragment(){
abstract val layoutId : T
private lateinit var binding : T
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<M> { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = T.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.lifecycleOwner = this.viewLifecycleOwner
}
There is a way to abstract out the particular ViewDataBinding, however, it would require to provide a layout resource reference for each concrete fragment implementation:
protected abstract val layoutResource: Int
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<M> { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, layoutResource, container, false).apply {
viewmodel = viewModel
}
return binding.root
}
I have my miewModel, which I'm injecting to my fragment throw ViewModelProviders.of(activity, viewModelFactory).get(MyViewModel::class.java).
It's work fine on first time of fragment creation, but if I will close my fragment, then I'will get an error "layout must not be null" which points on some of my layouts which I'm using in my fragment.
As I understood, this ishue happend because databinding still have some links to my fragment. So how to unbind it?
class MyFragment: Fragment(), Injectable {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity?.window?.changeStatusBarColor(this.requireContext(), R.color.yellow_status_bar)
val binding: MyLayoutBinding = DataBindingUtil.inflate(inflater, R.layout.my_layout, container, false)
binding.viewModel = viewModel
viewModel.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
}
}
ViewModelModule:
#Binds
#IntoMap
#ViewModelKey(MyViewModel::class)
internal abstract fun bindMyViewModel(myViewModel: MyViewModel): ViewModel
To bound ViewModel life cycle to fragment you need to call:
val vm = ViewModelProviders.of(fragnemt, viewModelFactory)[MyViewModel::class.java]
instead of:
val vm = ViewModelProviders.of(activity, viewModelFactory)[MyViewModel::class.java]
Don't forget to release resources in ViewModel.onCleared()