7

I have created an abstract BaseFragment class which will be extended by other concrete Fragment classes. I want to inject ViewModel in my BaseFragment using Koin. Here is my BaseFragment:

abstract class BaseFragment<out VM : BaseViewModel, DB : ViewDataBinding>(private val mViewModelClass: Class<VM>) : Fragment() {

    val viewModel: VM by viewModel()

    open lateinit var binding: DB

    fun init(inflater: LayoutInflater, container: ViewGroup) {
        binding = DataBindingUtil.inflate(inflater, getLayoutRes(), container, false)
    }

    open fun init() {}
    @LayoutRes
    abstract fun getLayoutRes(): Int

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View {
        init(inflater, container!!)
        init()
        super.onCreateView(inflater, container, savedInstanceState)
        return binding.root
    }

    open fun refresh() {}
} 

But I am not able to do so. I am using 2.0.1 version of Koin.

sagar suri
  • 4,351
  • 12
  • 59
  • 122
  • What error do you get? Btw your code can crash if `container` is `null`, I suggest you using a safe operator there just in case. – Javier Mendonça Jun 15 '19 at 06:39
  • I am getting `Cannot use VM as reified type parameter. Use a class Instead` @JavierMendonça – sagar suri Jun 15 '19 at 06:42
  • Yeah I was thinking something in those terms. I am not sure Koin can figure out what type `VM` is.. `VM` in this case should be reified so that Kotlin can infer the type so Koin can work, but you can't use `reified` in class definitions but functions. What is `mViewModelClass` bad what do you use it for? – Javier Mendonça Jun 15 '19 at 06:47
  • Earlier I was not using Dependency Injection and was passing the class type to the `ViewModelProviders` in BaseFragment. – sagar suri Jun 15 '19 at 06:50
  • 1
    I am afraid you won't be able to inject it generically that way without knowing what type `VM` is. Take out that code to each fragment, it's just one line of code ‍♂️. The data binding you can do that way though, just take care of the `container!!` – Javier Mendonça Jun 15 '19 at 06:52
  • 1
    Even though I am not sure it's worth to have the data binding done that way, you will need to cast it in each fragment where you use it. What you can do is do that in each fragment, and to make it more expressive you can use this extension function: `inline fun ViewGroup.bind(layoutId: Int, attachToRoot: Boolean = false): VD = DataBindingUtil.inflate(LayoutInflater.from(context), layoutId, this, attachToRoot)`. Then you just do `view.bind(R.layout.your_binding)`, very convenient. – Javier Mendonça Jun 15 '19 at 06:57
  • That sounds great! can you suggest me few links from where I can find suggest great implementations? As I am working with a team and I want everyone to follow the same structure i.e creating `ViewModel` and use data binding. How can I make it strict so that people get an error if they don't create `ViewModel` for their `Fragment`? – sagar suri Jun 15 '19 at 07:02
  • To "force" the other developers creating a viewModel you could have an `open val viewModel` in `BaseFragment` that you override in the implementation. Then you could have a function in the base that checks whether or not the viewModel has been initialized, if not through an error. Think kotlin `require()`. But it all depends whether or not the other devs use the `BaseFragment`, I think it's better just to talk about it, or reject PRs if not implemented. When it comes to resources, Android weekly and Kotlin weekly are great sources for learning about Android in general. – Javier Mendonça Jun 15 '19 at 09:07

2 Answers2

13

I have the same scenario in my case. You can also do like below:

Add your ViewModel as an abstract and set value when you extend your BaseFragment.

My BaseFragment have:

abstract class BaseFragment<Binding : ViewDataBinding, ViewModel : BaseViewModel> : Fragment() {
    protected abstract val mViewModel: ViewModel
    protected lateinit var bindingObject: Binding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        bindingObject = DataBindingUtil.inflate(inflater, getLayoutResId(), container, false)
        return bindingObject.root
    }

     /**
       * Get layout resource id which inflate in onCreateView.
      */
     @LayoutRes
     abstract fun getLayoutResId(): Int

     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        doDataBinding()
    }

     /**
      * Do your other stuff in init after binding layout.
      */
      abstract fun init()

     private fun doDataBinding() {
       bindingObject.lifecycleOwner = viewLifecycleOwner // it is extra if you want to set life cycle owner in binding
       // Here your viewModel and binding variable imlementation 
       bindingObject.setVariable(BR.viewModel, mViewModel)  // In all layout the variable name should be "viewModel"
       bindingObject.executePendingBindings()
       init()
}

}

Here is my actual Fragment implementation:

class FragmentComments : BaseFragment<FragmentCommentsBinding, FragmentCommentsVM>() {
// Here is the your viewmodel imlementation 
override val mViewModel: FragmentCommentsVM by viewModel() 

override fun getLayoutResId(): Int = [fragment layout id like "R.layout.fragment_com"]

override fun init() {
...
}

I hope this helps you. Let me know if more help required!

pRaNaY
  • 24,642
  • 24
  • 96
  • 146
2

I am currently solving the same issue, looking at source code of Koin, by viewModel() provides kotlin Lazy

/**
 * Lazy get a viewModel instance
 *
 * @param qualifier - Koin BeanDefinition qualifier (if have several ViewModel beanDefinition of the same type)
 * @param parameters - parameters to pass to the BeanDefinition
 * @param clazz
 */
fun <T : ViewModel> LifecycleOwner.viewModel(
        clazz: KClass<T>,
        qualifier: Qualifier? = null,
        parameters: ParametersDefinition? = null
): Lazy<T> = lazy { getViewModel(clazz, qualifier, parameters) }

which when initialized calls this other LifecycleOwner extension method that does the actual resolution of viewModel instance:

/**
 * Lazy getByClass a viewModel instance
 *
 * @param clazz - Class of the BeanDefinition to retrieve
 * @param qualifier - Koin BeanDefinition qualifier (if have several ViewModel beanDefinition of the same type)
 * @param parameters - parameters to pass to the BeanDefinition
 */
fun <T : ViewModel> LifecycleOwner.getViewModel(
        clazz: KClass<T>,
        qualifier: Qualifier? = null,
        parameters: ParametersDefinition? = null
): T {
    return getKoin().getViewModel(
            ViewModelParameters(
                    clazz,
                    this@getViewModel,
                    qualifier,
                    parameters = parameters
            )
    )
}

I havent tried it but it seems safe to say that if I call this method directly in my BaseFragment it should work the same way, my BaseFragment looks something like:

abstract class BaseFragment<VM : ViewModel> : Fragment() {

    lateinit var viewModel: VM
    abstract val viewModelClass: KClass<VM>

    override fun onCreate(savedInstanceState: Bundle?) {
        viewModel = getViewModel(clazz = viewModelClass)

        super.onCreate(savedInstanceState)
    }

}
estn
  • 1,203
  • 1
  • 12
  • 25