ViewModel SaveStateHandle with Koin DI - android

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

Can't Pass Bundle argument from Fragment to Viewmodel

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)

Activity-Fragment Communication w/ Hilt

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 :)

Android ViewModel Observe MutableLiveData<ArrayList<String>>

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.

How to generify data binding layout inflation?

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
}

How to unbind viewModel from activity when it's destroying

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

Categories

Resources