How to unbind viewModel from activity when it's destroying - android

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

Related

Kotlin inflate generic viewbinding class in parent

Aim is to declare a base class
abstract class BaseDialog<T : ViewBinding> : AppCompatDialogFragment() {
lateinit var binding: T
}
and all child classes should extend this parent class
class ChildClass: BaseDialog<ChildClassViewBinding>() {
}
Then I want to inflate the binding in parent class and save it to binding property
This seems out of my scope of knowledge of kotlin
Is this really possible to do?
If I were to do this, I'd do it like this:
class BaseDialogFragment<T: ViewBinding>(private val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> T)
: AppCompatDialogFragment() {
var _binding: T? = null
val binding: T get() = _binding ?: error("Must only access binding while fragment is attached.")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = bindingInflater(inflater, container, false)
return binding.root
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
with usage like:
class ChildClass: BaseDialog(ChildClassViewBinding::inflate) {
}
However, I would not do this in the first place (since there's a nice alternative). It can become messy pretty quickly to rely on inheritance for these kinds of things. What happens if you want to add some other features for a dependency injection framework, or some other common things you like to use? What if there's some features you like to use in some of your fragments but not all of them? And are you also creating base classes like this for Activity and non-Dialog Fragments?
These problems are why there's a programming axiom: "composition over inheritance".
Sometimes there's no choice but to use inheritance to avoid code duplication. But in the case of Fragments and Bindings, I don't think so. You can pass your layout reference to the super constructor, and use ViewBinding.bind() instead of inflate(). Since bindings rarely need to be accessed outside the onViewCreated function, you usually don't need a property for it.
class ChildClass: AppCompatDialogFragment(R.layout.child_class_view) {
override fun onViewCreated(view: View, bundle: Bundle?) {
super.onViewCreated(view, bundle)
val binding = ChildClassViewBinding.bind(view)
//...
}
}
If you do need a property for it, #EpicPandaForce has a library that makes it a one-liner and handles the leak-avoidance on destroy for you inside a property delegate.
Library here
Usage:
class ChildClass: AppCompatDialogFragment(R.layout.child_class_view) {
private val binding by viewBinding(ChildClassViewBinding::bind)
}
Create Base Fragment
abstract class BaseFragment<VB : ViewBinding> : Fragment() {
private var _bi: VB? = null
protected val bi: VB get() = _bi!!
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_bi = bindingInflater(inflater, container, false)
return _bi!!.root
}
override fun onDestroyView() {
super.onDestroyView()
_bi = null
}
}
In your Child Fragment
class HomeFragment : BaseFragment<HomeFragmentBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> HomeFragmentBinding
get() = HomeFragmentBinding::inflate
}

How to set MainViewModel?

I have my MainActivity where there are the movie with their poster. If I click on a poster (in the layout there are 4 ImageView) I can read the plot (a TextView) that is in the Fragment. If I open my app with the emulator MainActivity and Fragment are overlapping so I need to set the ViewModel. How to set it?
//MainActivity
class JacksonActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.jackson_activity)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.jackson_film, JacksonFragment.newInstance())
.commitNow()
}
}
//Fragment
class JacksonFragment : Fragment() {
companion object {
fun newInstance() = JacksonFragment()
}
private lateinit var viewModel: MainViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
return inflater.inflate(R.layout.jackson_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
// TODO: Use the ViewModel
} }
ViewModelProvider is deprecated. more info: ViewModelProvider
To implement viewModel simply use val viewModel: MainViewModel by viewmodels()
if you use the same viewmodel in more than one place(acitity/fragment) in fragment use val viewModel: MainViewModel by activityViewModels()
Note: do not forget to update the dependencies. More info: ViewModel / SharedViewModel

ViewModel SaveStateHandle with Koin DI

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?

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 create new instance of viewModel in koin every time

Am Using Koin as Dependency injection pattern in my project, I need to create new instances whenever i load fragment/activity, now am using the following pattern, Any solution for that it might save lots of time.
private val homeViewModel: HomeViewModel by viewModel()
The question is why you want new instances everytime? The whole concept of ViewModel is to retain the same instance and data. viewModel {} creates new instance everytime you inject it unless it is not shared.
Don't know why it is not working for you, but I think you can use factory{} instead of viewModel{}.
factory{
// this is because you need new instance everytime.
HomeViewModel()
}
Define ViewModel as an abstract in BaseFragment class and set value when you extend your BaseFragment.
abstract class BaseFragment<Binding : ViewDataBinding, ViewModel : BaseViewModel> : Fragment(){
protected var bindingObject: Binding? = null
protected abstract val mViewModel: ViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
bindingObject = DataBindingUtil.inflate(inflater, getLayoutResId(), container, false)
return bindingObject?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
performDataBinding()
}
#LayoutRes
abstract fun getLayoutResId(): Int
private fun performDataBinding() {
bindingObject?.setLifecycleOwner(this)
bindingObject?.setVariable(BR.viewModel, mViewModel)
bindingObject?.executePendingBindings()
}
}
And in your fragment
class FragmentNew : BaseFragment<FragmentNewBinding, FragmentNewVM>() {
// Here is the your viewmodel imlementation. Thus when you create fragment it's by default override method
override val mViewModel: FragmentNewVM by viewModel()
override fun getLayoutResId(): Int = [fragment layout id like "R.layout.fragment_new"]
}
You are going to want to forego using by viewmodel and instantiate the class directly. You can get global (scoped) variables through getKoin().get().
private val viewModel = HomeViewModel(getKoin().get())

Categories

Resources