Kotlin generic properties issue - android

I got some issues with Kotlin when translating my android project from java to Kotlin.
Say i have interface I and interface O which extends interface I.
interface I{
}
interface O: I{
}
And generic class A which have generic parameter V that extends interfaceI, and generic class B which extends class A:
abstract class A<V: I> {
}
class B : A<O>() {
}
When i'm trying to create such property:
val returnB: A<I>
get() = b
I'm getting compiler error 'required A, found B'. In Java this will work without any issues. How can i access this using Kotlin ?
I need to use this approach for Basic classes in my application.
BaseViewModel which have generic parameter for Navigator class:
abstract class BaseViewModel<N>(application: Application, val repositoryProvider:
RepositoryProvider) : AndroidViewModel(application) {
var navigator: N? = null
fun onDestroyView() {
navigator = null
}
open fun onViewAttached() {
}
}
BaseActivity class:
abstract class BaseActivity<T : ViewDataBinding, V : BaseViewModel<BaseNavigator>> : AppCompatActivity(),
BaseFragment.Callback, BaseNavigator {
// .......
private var mViewModel: V? = null
/**
* Override for set view model
* #return view model instance
*/
abstract val viewModel: V
// .......
}
BaseNavigator interface uses for VM - View communication:
interface BaseNavigator {
fun invokeIntent(intent: Intent?, b: Bundle?, c: Class<*>?,
forResult: Boolean, requestCode: Int)
fun replaceFragment(fragment: Fragment, addToBackStack: Boolean)
fun showDialogFragment(fragment: DialogFragment?, tag: String?)
fun showToast(message: String?)
}
Here example code where i'm extending these classes:
AuthViewModel:
class AuthViewModel(context: Application, repositoryProvider: RepositoryProvider) :
BaseViewModel<AuthNavigator>(context,repositoryProvider) {
// ....
}
AuthNavigator:
interface AuthNavigator : BaseNavigator {
fun requestGoogleAuth(code: Int)
fun requestFacebookAuth(callback: FacebookCallback<LoginResult>)
}
And AuthActivity class where error was appeared:
class AuthActivity : BaseActivity<ActivityAuthBinding, BaseViewModel<BaseNavagator>>(),
GoogleApiClient.OnConnectionFailedListener, AuthNavigator {
#Inject
lateinit var mViewModel: AuthViewModel
override val viewModel: BaseViewModel<BaseNavigator>
get() = mViewModel // Required:BaseViewModel<BaseNavigator> Found: AuthViewModel
}
I'm also tried to change generic parameter in AuthActivity from BaseViewModel to AuthViewModel, but compiler throws error 'required BaseViewModel'.
And i tried to change
override val viewModel: BaseViewModel<BaseNavigator>
get() = mViewModel
to
override val viewModel: AuthViewModel
get() = mViewModel
but in this case compiler throws error 'Property type is 'AuthViewModel', which is not a subtype type of overridden'.
update:
That works when i add out property to BaseViewModel:
BaseViewModel<out N : BaseNavigator>
But in this case i can only create
private var navigator: N? = null
which i need to be public so i can set it in the Activity class. Can i create public setter for this property? When i'm trying to create setter an error occurs:
private var navigator: N? = null
fun setNavigator(n: N) { // error: Type parameter N is declared as 'out' but occurs in 'in' position in type N
navigator = n
}

It looks like you are expecting the type parameter to behave covariantly. Kotlin uses declaration-site variance. If you do not specify the variance, generic type parameters are invariant.
In other words, right now there is no relationship between A<I> and A<O>. But if you declare
abstract class A<out V : I>
then A<O> is a subtype of A<I>.
(There is also <in> for contravariance, which works the other way around. See https://kotlinlang.org/docs/reference/generics.html for more details.)

Related

How to pass arguments from Activity to ViewModel using Hilt (without a ViewModel Factory)

In my activity, I have multiple variables being initiated from Intent Extras. As of now I am using ViewModelFactory to pass these variables as arguments to my viewModel.
How do I eliminate the need for ViewModelFacotory with hilt
Here are two variables in my Activity class
class CommentsActivity : AppCompatActivity() {
private lateinit var viewModel: CommentsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
val contentId = intent.getStringExtra(CONTENT_ID_FIELD) //nullable strings
val highlightedCommentId = intent.getStringExtra(HIGHLIGHTED_COMMENT_ID_RF) //nullable strings
val commentsViewModelFactory = CommentsViewModelFactory(
contentId,
highlightedCommentId
)
viewModel = ViewModelProvider(this, commentsViewModelFactory[CommentsViewModel::class.java]
}
}
Here is my viewModel
class CommentsViewMode(
contentId : String?,
highlightedCo;mmentId : String?,
) : ViewModel() {
//logic code here
}
My app is already set up to use hilt but in this case How can I pass these 2 variables and eliminate the viewModelFactory entirely
The trick is to initialize those variables only once, while the activity can be created multiple times. In my apps, I use a flag.
View model:
class CommentsViewModel : ViewModel() {
private var initialized = false
private var contentId : String? = null
private var highlightedCommentId : String? = null
fun initialize(contentId : String?, highlightedCommentId : String?) {
if (!initialized) {
initialized = true
this.contentId = contentId
this.highlightedCommentId = highlightedCommentId
}
}
//logic code here
}
Also, you should know that there is an open issue in dagger project exactly for this capability:
https://github.com/google/dagger/issues/2287
You're welcome to follow the progress.
If you want to use hilt effectively u can follow this steps
Use #HiltViewModel in your view model
#HiltViewModel
class MyViewModel #inject constructor(private val yrParameter): ViewModel {}
Also you no longer need any ViewModelFactory! All is done for you! In your activity or fragment, you can now just use KTX viewModels() directly.
private val viewModel: MyViewModel by viewModels()
Or if you want to use base classes for fragment and activity you can use this code to pass viewModel class
abstract class BaseFragment<V: ViewModel, T: ViewDataBinding>(#LayoutRes val layout: Int, viewModelClass: Class<V>) : Fragment() {
private val mViewModel by lazy {
ViewModelProvider(this).get(viewModelClass)
}
}

Android: Dagger2 binding with matching key exists in components, existing class that didn't previously need #Provides-annotated method now needs one

We are attempting to develop a new Activity for an existing Fragment, the compiler is throwing errors for an existing FragmentComponentBuilder class that other Dagger modules have had no issues with utilizing in the past, but because of the code we introduced, is now failing to build. The error raised for the FragmentComponentBuilder details not being able to be provided because of a missing #Provides-annotated, also binding with matching key existing in components related to the new Activity.
Here is the error:
/app/application/AppComponent.java:8: error: [Dagger/MissingBinding] java.util.Map<java.lang.Class<? extends androidx.fragment.app.Fragment>,javax.inject.Provider<app.dagger.fragment.FragmentComponentBuilder<?,?>>> cannot be provided without an #Provides-annotated method.
public abstract interface AppComponent {
^
A binding with matching key exists in component: app.CaComponent
java.util.Map<java.lang.Class<? extends androidx.fragment.app.Fragment>,javax.inject.Provider<app.dagger.fragment.FragmentComponentBuilder<?,?>>> is injected at
app.DevicesActivity.fragmentComponentBuilders
app.DevicesActivity is injected at
app.application.AppComponent.inject(app.DevicesActivity)
It is also requested at:
app.DevicesActivity.fragmentComponentBuilders
The following other entry points also depend on it:
dagger.MembersInjector.injectMembers(T) [app.application.AppComponent → app.DevicesActivityComponent]
An important part about the new DevicesActivity we're developing, is that the associated DevicesFragment logic needs to remain relatively unchanged, because the core logic that handles the presentation of this screen is a Fragment -> Fragment interaction. The DevicesActivity handles presentation from another Activity, and contains restricted view properties as opposed to the Fragment -> Fragment presentation.
The newly created DevicesActivity class:
class DevicesActivity: InjectableActivity(), CaView, HasFragmentSubComponentBuilders{
#Inject
lateinit var fragmentComponentBuilders: Map<Class<out Fragment>, #JvmSuppressWildcards Provider<FragmentComponentBuilder<*, *>>>
var caModel: CaModel? = null
private val devicesFragment = DevicesFragment.newInstance()
companion object {
val kCaModel = "CA_MODEL_KEY"
fun create(context: Context, caModel: CaModel) : Intent {
val intent = Intent(context, DevicesActivity::class.java)
intent.putExtra(kCaModel, caModel)
return intent
}
}
override fun injectActivity(hasActivitySubComponentBuilders: HasActivitySubcomponentBuilders): ActivityComponent<DevicesActivity> {
val builder = hasActivitySubComponentBuilders.getBuilder(DevicesActivity::class.java)
val componentBuilder = builder as DevicesActivityComponent.Builder
val component = componentBuilder.activityModule(DevicesActivityModule())?.build()
component!!.injectMembers(this)
return component
}
override fun onStart(){
super.onStart()
supportFragmentManager.beginTransaction().replace(android.R.id.content, devicesFragment).commit()
}
override fun getBuilder(fragmentClass: Class<out Fragment>): FragmentComponentBuilder<*, *> =
fragmentComponentBuilders.getValue(fragmentClass).get()
override fun onLoggedOut(wasLoggedOutDueToInactivity: Boolean, wasLoggedOutDueToBadCreds: Boolean) {
}
}
DevicesActivityComponent for new Activity:
#Scope
#Retention(AnnotationRetention.RUNTIME)
annotation class DevicesScope
#DevicesScope
#Subcomponent(modules = [DevicesActivityModule::class])
interface DevicesActivityComponent : ActivityComponent<DevicesActivity> {
#Subcomponent.Builder
interface Builder : ActivityComponentBuilder<DevicesActivityModule, DevicesActivityComponent>
}
The DevicesActivityModule for new Activity:
#Module
class DevicesActivityModule : ActivityModule<DevicesActivity>() {
#Provides
fun presenter(caSubject: CaSubject, interactor: ST)
: DevicesPresenter = DevicesPresenter(caSubject, interactor)
#Provides
fun interactor(schedulerProvider: SchedulerProvider) : ST =
ST(schedulerProvider)
}
The associated DevicesFragment class:
class DevicesFragment : MvpScopedFragment<DevicesView, DevicesPresenter>(), DevicesView {
lateinit var binding : DevicesBinding
private lateinit var adapter : DevicesAdapter
lateinit var devicesDialog: AlertDialog
companion object {
fun newInstance() : DevicesFragment = DevicesFragment()
}
override fun inject(hasFragmentSubComponentBuilders: HasFragmentSubComponentBuilders): DevicesPresenter {
val builder = hasFragmentSubComponentBuilders.getBuilder(DevicesFragment::class.java)
val componentBuilder = builder as DevicesFragmentComponent.Builder
val component = componentBuilder.module(DevicesFragmentModule(this)).build()
return component.presenter()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.respec_devices, container, false)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.caModel.screen = CaScreen.DEVICES
presenter.save()
binding.header.caModel = presenter.caModel
binding.caModel = presenter.caModel
presenter.setDelegate()
if(presenter.pm.hasProfile()) {
presenter.profile = presenter.pm.getProfile()!!
}
}
The CaComponent class that the error output references as a class that contains a binding with a matching key to other components:
#Scope
#Retention(AnnotationRetention.RUNTIME)
annotation class CaScope
#CaScope
#Subcomponent(modules = [CaModule::class, FragmentBindingModule::class])
interface CaComponent : ActivityComponent<CaActivity> {
fun presenter() : CaPresenter
#Subcomponent.Builder
interface Builder : ActivityComponentBuilder<CaModule, CaComponent>
}
FragmentComponentBuilder that has remained unchanged:
interface FragmentComponentBuilder<M : FragmentModule<*>, C : FragmentComponent<*>> {
fun module(activiyModule: M) : FragmentComponentBuilder<M, C>
fun build() : C
}
ActivityComponentBuilder that has remained unchanged:
interface ActivityComponentBuilder<M : ActivityModule<*>, C : ActivityComponent<*>> {
fun activityModule(activityModule: M): ActivityComponentBuilder<M, C>?
fun build(): C
}
AppComponent that contains a newly created inject method that takes the DevicesActivity as a parameter:
#Singleton
#Component(modules = [AppModule::class, ActivityBindingModule::class])
interface AppComponent {
fun inject(app: App)
fun inject(printingService: PrintingService)
fun context() : Context
fun preferenceFactory() : PreferenceFactory
fun inject(devicesActivity: DevicesActivity)
}
Please let me know if you need to see any additional files not included here that would assist in troubleshooting. Thank you for whatever help you're willing to give!

Is it good practice for a Jetpack ViewModel to implement an interface?

In a project every Jetpack Viewmodel implements an interface. For example:
interface ExamReportViewModel : ActionSource<ExamReportViewModel.Action>,
ExamExamineeListItem.Listener {
val examReportId: StateFlow<String?>
val examReportHeader: StateFlow<ExamReportHeader?>
val examExamineeList: StateFlow<List<ExamExamineeListItem>>
val isHeaderExpanded: StateFlow<Boolean>
fun setExamReportId(id: String)
fun toggleHeaderExpanded()
fun navigateToExtraordinaryEvent()
sealed class Action {
data class ToIdentificationDialog(val examReportId: String, val examineeId: String) : Action()
data class ToEvaluation(val exam: Exam) : Action()
object ToExtraordinaryEvent : Action()
}
}
An actual implementation:
class ExamReportViewModelImpl #Inject constructor(
private val examReportInteractor: ExamReportInteractor,
private val errorDelegate: ErrorDelegate,
) : BaseViewModel(), ExamReportViewModel
Does this make sense? What would be the cons and pros?
Cons:
The chances that the same view model requires 2 different implementations are close to 0
we can hide the MutableStateFlow implementation in a private field inside the viewmodel and just us .asStateFlow() to convert it to a non mutable one
it needs to be declared
they are testable with and without the interface
it makes injecting with dagger hilt complicated, because there is a BaseFragment which is based on generics
Are there any pros?
abstract class BaseFragment<T : Any, B : ViewDataBinding>(
open val contentViewId: Int,
) : DaggerFragment() {
#Inject
lateinit var viewModel: T
}
class ExamReportFragment :
BaseFragment<ExamReportViewModel, FragmentExamReportBinding>(R.layout.fragment_exam_report) {
}
With this approach I was not able to use the
private val viewModel by viewModels<TasksViewModel>()
extension. I don't know if it's possible or if it makes sense.

Getting generic class from Kotlin Generic

I have the following class signature:
abstract class BaseActivity<E : ViewModel> : AppCompatActivity() {
protected lateinit var viewModel: E
}
Now I want to initialize my viewModel in a generic way using ViewModelProvider, so:
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(MyViewModel::class)
Given that MyViewModel class will be provided in the generic type, I'd say this could potentially be abstracted into the BaseActivity so I dont have to do it for every Activity that extends it.
I tried with:
inline fun <reified E : ViewModel> getViewModelClass() = E::class.java
But then when doing:
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(getViewModelClass())
I get Cannot use E as reified type paramter. Use class instead
Is there a solution to this?
E in BaseActivity can't be reified, so you can't pass it to any methods which take a reified E.
Your best option may just be to accept the class as a constructor parameter.
abstract class BaseActivity<E : ViewModel>(private val modelClass: Class<E>) : AppCompatActivity() {
protected lateinit var viewModel: E
... viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(modelClass)
}
If BaseActivity wasn't abstract, you could add a factory method:
// outside BaseActivity class itself
fun <reified E : BaseModel> BaseActivity() = BaseActivity(E::class.java)
but it wouldn't help when extending it.
You ca do it in this way:
abstract class BaseActivity<E : ViewModel> : AppCompatActivity() {
protected lateinit var viewModel: E
abstract fun getViewModel():E
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(getViewModel())
}
}
Now, you can extend any class from BaseActivity and override the getViewModel() function returning the respective ViewModel class.
Hope this helps.
EDIT
Try this once:
inline fun <reified E> getViewModelClass(): Class<E> {
return E::class.java
}
and use it like this:
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(getViewModelClass())
https://stackoverflow.com/a/52107111/8832537
You should check this guy's solution. The only downside of his approach is he uses Reflection API to get the generic parameter. I researched a lot but didn't find a solution that doesn't use reflection. If you did find it, let me know. That would be more convenient.

Android Parcelable in Kotlin: CREATOR not found on Parcelable data class

With the release of the Kotlin RC, I started writing an app to learn it however I can not figure out how to get Parcelable to work.
the data class:
data class Project (val reponame:String,
val username:String,
val language:String,
val vcsUrl:String,
val branches:Map<String, Branch>) : Parcelable {
companion object {
val CREATOR = object : Parcelable.Creator<Project> {
override fun createFromParcel(`in`: Parcel): Project {
return Project(`in`)
}
override fun newArray(size: Int): Array<Project?> {
return arrayOfNulls(size)
}
}
}
protected constructor(parcelIn: Parcel) : this (
parcelIn.readString(),
parcelIn.readString(),
parcelIn.readString(),
parcelIn.readString(),
mapOf<String, Branch>().apply {
parcelIn.readMap(this, Branch::class.java.classLoader)
}
)
override fun describeContents(): Int {
throw UnsupportedOperationException()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(reponame)
dest.writeString(username)
dest.writeString(language)
dest.writeString(vcsUrl)
dest.writeMap(branches)
}
}
Reading it:
class ProjectDetailActivity : BaseActivity() {
lateinit var project: Project
companion object {
const val EXTRA_PROJECT = "extra_project"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
project = intent.extras.getParcelable(EXTRA_PROJECT)
tvTitle.text = project.reponame
}
}
The exception:
Caused by: android.os.BadParcelableException: Parcelable protocol requires a Parcelable.Creator object called CREATOR on class com.eggman.circleciandroid.model.Project
at android.os.Parcel.readParcelableCreator(Parcel.java:2415)
at android.os.Parcel.readParcelable(Parcel.java:2337)
at android.os.Parcel.readValue(Parcel.java:2243)
at android.os.Parcel.readArrayMapInternal(Parcel.java:2592)
at android.os.BaseBundle.unparcel(BaseBundle.java:221)
at android.os.BaseBundle.get(BaseBundle.java:281)
at com.eggman.circleciandroid.ui.ProjectDetailActivity.onCreate(ProjectDetailActivity.kt:22)
I am sure it is something simple I am missing, has anyone else had success with Parcelable on latest Kotlin?
Kotlin Version: 1.0.0-rc-1036
Kotlin Plugin Version: 1.0.0-rc-1036-IJ143-4
Code is viewable # https://github.com/eggman87/circle-kotlin
Kotlin RC dropped previously deprecated generation of static fields for all companion object properties (learn more in this answer).
Now only those marked by const, lateinit or #JvmField will have a static field generated.
You need to annotate val CREATOR by #JvmField annotation since Android Framework expects a static field CREATOR in your class.
Here you have some useful Kotlin extension functions that will help you to create your CREATORs and also some examples (using data classes and list inside the data class)
Gist: Data Class & Parcelables example
I'm using this code in an Android App: (link)
The same code you can find it here: (link)

Categories

Resources