Reputation: 3097
Hey I want to create BaseFragment class that gets viewModel by generic type:
abstract class BaseFragment<B : ViewDataBinding, VM : ViewModel> : DaggerFragment() {
val viewModel by viewModels<VM> { viewModelFactory }
...
}
// Native function
@MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)
but getting error Cannot use 'VM' as reified type parameter. Use a class instead.
is it at all possible to achieve what I am trying to do? Maybe with other approach?
Upvotes: 5
Views: 2027
Reputation: 3097
There dirty way to get ViewModel and ViewBinding only from generics:
abstract class BaseFragment<BINDING : ViewDataBinding, VM : ViewModel> : Fragment() {
val viewModel by viewModels(getGenericClassAt<VM>(1))
var binding: BINDING? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
binding = inflater.inflateBindingByType<BINDING>(container, getGenericClassAt(0)).apply {
lifecycleOwner = this@BaseFragment
}.also {
it.onBindingCreated()
}
return binding?.root
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = false
internal open fun BINDING.onBindingCreated() {}
fun <T> withBinding(action: BINDING.() -> T): T? = binding?.let { action(it) }
}
@Suppress("UNCHECKED_CAST")
fun <CLASS : Any> Any.getGenericClassAt(position: Int): KClass<CLASS> =
((javaClass.genericSuperclass as? ParameterizedType)
?.actualTypeArguments?.getOrNull(position) as? Class<CLASS>)
?.kotlin
?: throw IllegalStateException("Can not find class from generic argument")
fun <BINDING : ViewBinding> LayoutInflater.inflateBindingByType(
container: ViewGroup?,
genericClassAt: KClass<BINDING>
): BINDING = try {
@Suppress("UNCHECKED_CAST")
genericClassAt.java.methods.first { inflateFun ->
inflateFun.parameterTypes.size == 3
&& inflateFun.parameterTypes.getOrNull(0) == LayoutInflater::class.java
&& inflateFun.parameterTypes.getOrNull(1) == ViewGroup::class.java
&& inflateFun.parameterTypes.getOrNull(2) == Boolean::class.java
}.invoke(null, this, container, false) as BINDING
} catch (exception: Exception) {
throw IllegalStateException("Can not inflate binding from generic")
}
And usage:
class BoardFragment : BaseFragment<FragmentBoardBinding, BoardViewModel>() {
override fun FragmentBoardBinding.onBindingCreated() {
viewModel = [email protected]
}
}
Dirty, but saves tones of coding
Upvotes: 1
Reputation: 19
There is clearer solution:
abstract class BaseActivity<VM : BaseViewModel> : AppCompatActivity {
protected val viewModel: VM by viewModel(clazz = getViewModelClass())
private fun getViewModelClass(): KClass<VM> = (
(javaClass.genericSuperclass as ParameterizedType)
.actualTypeArguments[0] as Class<VM>
).kotlin
}
And usage:
class MainActivity : BaseActivity<MainViewModel>(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onViewCreated()
}
}
Upvotes: 1
Reputation: 3097
Found working way, but is it clean enough?
abstract class BaseModelFragment<VM : ViewModel>(viewModelClass: KClass<VM>) : DaggerFragment() {
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
val viewModel by viewModel(viewModelClass) { viewModelFactory }
private fun Fragment.viewModel(
clazz: KClass<VM>,
ownerProducer: () -> ViewModelStoreOwner = { this },
factoryProducer: (() -> ViewModelProvider.Factory)? = null,
) = createViewModelLazy(clazz, { ownerProducer().viewModelStore }, factoryProducer)
}
And usage:
open class SomeFragment : BaseModelFragment<CustomerSupportViewModel>(CustomerSupportViewModel::class) {
...
}
It is tested and working. Any ideas how to improve it? :)
Upvotes: 2