Parcelized sealed class with private constructor throws inaccessible error when getting from Intent in new activity but works everywhere else?

Viewed 4830

I made a special sealed class to validate my nullable URLs so I can safely and easily know it's state (None, Invalid, Valid)

sealed class UrlType : Parcelable {
    @Parcelize class Valid private constructor(val url: String) : UrlType() {
        companion object : CompanionTest<UrlType.Valid, String>(::Valid)
    }

    @Parcelize object None : UrlType()
    @Parcelize class Invalid(val invalidUrl: String) : UrlType()

    companion object {

        fun getUrlType(url: String?): UrlType {
            return if (url == null) {
                UrlType.None
            } else if (!url.isValidUrl()) {
                UrlType.Invalid(url)
            } else {
                Valid.create(url)
            }
        }
    }
}

open class CompanionTest<out T, in A>(creator: (A) -> T) {

    private var creator: ((A) -> T)? = creator

    fun create(arg1: A): T {
        return creator!!(arg1)
    }
}

@Parcelize
data class NewsPost(
        val id: String,
        val title: String,
        val description: String?,
        val webContentUrl: UrlType,
        val imageUrl: UrlType,
        val created: String,
        val updated: String,
        val feeds: List<Feed>
) : Parcelable

When I was first trying out the idea, I had my suspicions that it wouldn't work because of the private constructor and Parcelize but I did some tests and it did work.

I also have a sealed class to indicate a launch type for my activity which can be entered from multiple paths

sealed class NewsLaunchType : Parcelable {
    @Parcelize object FeedList : NewsLaunchType()
    @Parcelize class FeedDetail(val newsFeedType: NewsFeedType) : NewsLaunchType()
    @Parcelize class PostDetail(val newsPost: NewsPost) : NewsLaunchType()
}

When I go to launch my activity with a NewsLaunchType.PostDetail(newsPost) the app is crashing with the following error

fun startActivity(){
    val intent = Intent(context, NewsActivity::class.java).apply {                      
      putExtra("key", NewsLaunchType.PostDetail(newsPost))
    }      
    startActivity(intent)
}

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_news)

        val newsActivityLaunchType = intent.getParcelableExtra<NewsLaunchType>("key")
}

2018-11-08 11:08:33.897 17030-17030/com.something.internal E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.something.internal, PID: 17030
    java.lang.IllegalAccessError: Method 'void com.something.somethingkit.helper.UrlType$Valid.<init>(java.lang.String)' is inaccessible to class 'com.something.somethingkit.helper.UrlType$Valid$Creator' (declaration of 'com.something.somethingkit.helper.UrlType$Valid$Creator' appears in /data/app/com.something.internal-QTiupS4yw25rZK5uXP7UCQ==/base.apk:classes2.dex)
        at com.something.somethingkit.helper.UrlType$Valid$Creator.createFromParcel(Unknown Source:11)
        at android.os.Parcel.readParcelable(Parcel.java:2798)
        at com.something.somethingkit.model.news.local.NewsPost$Creator.createFromParcel(Unknown Source:25)
        at android.os.Parcel.readParcelable(Parcel.java:2798)
        at com.something.screens.news.NewsLaunchType$PostDetail$Creator.createFromParcel(Unknown Source:13)
        at android.os.Parcel.readParcelable(Parcel.java:2798)
        at android.os.Parcel.readValue(Parcel.java:2692)
        at android.os.Parcel.readArrayMapInternal(Parcel.java:3059)
        at android.os.BaseBundle.unparcel(BaseBundle.java:257)
        at android.os.Bundle.getParcelable(Bundle.java:888)
        at android.content.Intent.getParcelableExtra(Intent.java:7734)
        at com.something.screens.news.NewsActivity.onCreate(NewsActivity.kt:31)
        at android.app.Activity.performCreate(Activity.java:7183)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1220)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2908)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3030)
        at android.app.ActivityThread.-wrap11(Unknown Source:0)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1696)
        at android.os.Handler.dispatchMessage(Handler.java:105)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6938)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

Now that's not entirely unexpected, except that it does work if I try to pull it out within the same location I created it in

 fun startActivity(){
        val intent = Intent(context, NewsActivity::class.java).apply {                      
          putExtra("key", NewsLaunchType.PostDetail(newsPost))
        }      

        val test = intent.getParcelableExtra<NewsLaunchType>("key")
        startActivity(intent)
    }

It also works if I add it to a fragment bundle, and add/replace a new fragment into my activity.

companion object {
    fun newInstance(newsPost: NewsPost): NewsDetailFragment {
                val bundle = Bundle()
                bundle.putParcelable(extraNewsPost, newsPost)

                val fragment = NewsDetailFragment()
                fragment.arguments = bundle
                return fragment
            }
}

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val newsPost = arguments?.getParcelable<NewsPost>(extraNewsPost)

}

Note - When passing it across activities, there is an extra layer of Sealed Classes going on, but it will crash as well if I simply tried to pass a NewsPost across activities, so the extra layer is not the issue.

This seems like it might be a bug with the Kotlin Parcelize feature.

Is there any reason why passing it across activities would throw an error but passing it between fragments doesn't? If there is a reason... any suggestions on how I could pass this across activities?

1 Answers

The docs clearly state:

The primary constructor should be accessible (non-private)

Not sure why it's working when passing to fragment and not working when passing to activity. In my case, I solved this issue by making my class constructor internal.

Related