Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IllegalStateException in restoreViewModels on startup. #212

Closed
jQrgen opened this issue Mar 19, 2019 · 6 comments
Closed

IllegalStateException in restoreViewModels on startup. #212

jQrgen opened this issue Mar 19, 2019 · 6 comments

Comments

@jQrgen
Copy link

jQrgen commented Mar 19, 2019

Hello

When my users restart/re-open (Not sure which one) the app i get the following error report in firebase crashlytics:

Fatal Exception: java.lang.RuntimeException: Unable to start activity 
ComponentInfo{getflareapp.com.sosterTilSoster/getflareapp.com.sosterTilSoster.MainActivity}: 
java.lang.IllegalStateException: You are trying to call restoreViewModels but you never called saveViewModels!
       at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2858)
       at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2933)
       at android.app.ActivityThread.-wrap11(Unknown Source)
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1612)
       at android.os.Handler.dispatchMessage(Handler.java:105)
       at android.os.Looper.loop(Looper.java:164)
       at android.app.ActivityThread.main(ActivityThread.java:6710)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:770)

I'm having a hard time reproducing the error in development.

The project has several AndroidX dependencies. Maybe this could be the problem?

...
    implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0-rc02"
    implementation "android.arch.navigation:navigation-ui-ktx:1.0.0-rc02"

    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha3'
    implementation('androidx.recyclerview:recyclerview:1.0.0')
    implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"
    implementation 'androidx.exifinterface:exifinterface:1.0.0'
...

Maybe the following issue regarding MvRx and AndroidX is related?

#211

Thanks for any help!

@jQrgen jQrgen changed the title IllegalStateException in restoreViewModels on restart. IllegalStateException in restoreViewModels on startup. Mar 19, 2019
@rossbacher
Copy link
Collaborator

@jQrgen did you build your own MvRx binary? We did not release this yet.

This looks like a some state where our view model did not get save but you try to restore them.

@jQrgen
Copy link
Author

jQrgen commented Mar 20, 2019

I did not build my own binary. My mistake was assuming that MvRx was androidx ready.

Now i have an app in production using androidx and MvRx so I guess i could pull master since it has the merged androidx update and try building my own binary?

@gpeal
Copy link
Collaborator

gpeal commented Mar 20, 2019

@jQrgen It's fine to use mvrx with androidx. Do you have your own BaseFragment or do you use BaseMvRxFragment that ships with MvRx directly?

@jQrgen
Copy link
Author

jQrgen commented Mar 24, 2019

Basically i'm using a home-grown version of the setup in the official example. Not too sure if it is best practice and how robust it the setup is.

I have inherited from BaseMvRxFragment into my own BaseFragment class

BaseFragment.kt

abstract class BaseFragment: BaseMvRxFragment() {
    val currentUserSessionViewModel: CurrentUserSessionViewModel by activityViewModel ()
    val chatListViewModel: ChatListViewModel by activityViewModel ()
    val allUsersViewModel: AllUsersViewModel by activityViewModel ()
    val newsPostsViewModel: NewsPostsViewModel by activityViewModel ()
    val chatViewModel: ChatViewModel by activityViewModel ()
    val markersViewModel: MarkersViewModel by activityViewModel ()

    protected fun navigate(@IdRes id: Int, args: Parcelable? = null) {
        findNavController().navigate(id, Bundle().apply { putParcelable(MvRx.KEY_ARG, args) })
    }

    protected fun hideSoftKeyboard() {
        val context = activity?.applicationContext ?: return
        val currentView = view ?: return
        val inputMethodManager = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
        inputMethodManager.hideSoftInputFromWindow(currentView.windowToken, 0)
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        // Forward results to EasyPermissions
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
    }

}

Then i have inherited from the BaseFragment into a BaseEpoxyFragment class

BaseEpoxyFragment.kt

abstract class BaseEpoxyFragment : BaseFragment() {

    private var recyclerView: EpoxyRecyclerView? = null
    protected val epoxyController by lazy { epoxyController() }

    abstract fun epoxyController(): MvRxEpoxyController

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

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_base_mvrx, container, false).apply {
            recyclerView = findViewById(R.id.recycler_view)
            recyclerView?.setController(epoxyController)
        }
    }

    override fun invalidate() {
        recyclerView?.requestModelBuild()
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        epoxyController.onSaveInstanceState(outState)
    }

    override fun onDestroyView() {
        epoxyController.cancelPendingModelBuild()
        super.onDestroyView()
    }
}

I have used MvRxEpoxyController from the example which i have limited understanding of and have never touched.

MvRxEpoxyController.kt

/**
 * For use with [BaseEpoxyFragment.epoxyController].
 *
 * This builds Epoxy models in a background thread.
 */
open class MvRxEpoxyController(
    val buildModelsCallback: EpoxyController.() -> Unit = {}
) : AsyncEpoxyController() {

    override fun buildModels() {
        buildModelsCallback()
    }
}


/**
 * Create a [MvRxEpoxyController] that builds models with the given callback.
 */
fun BaseEpoxyFragment.simpleController(
    buildModels: EpoxyController.() -> Unit
) = MvRxEpoxyController {
    // Models are built asynchronously, so it is possible that this is called after the fragment
    // is detached under certain race conditions.
    if (view == null || isRemoving) return@MvRxEpoxyController
    buildModels()
}

/**
 * Create a [MvRxEpoxyController] that builds models with the given callback.
 * When models are built the current state of the viewmodel will be provided.
 */
fun <S : MvRxState, A : MvRxViewModel<S>> BaseEpoxyFragment.simpleController(
    viewModel: A,
    buildModels: EpoxyController.(state: S) -> Unit
) = MvRxEpoxyController {
    if (view == null || isRemoving) return@MvRxEpoxyController
    withState(viewModel) { state ->
        buildModels(state)
    }
}

/**
 * Create a [MvRxEpoxyController] that builds models with the given callback.
 * When models are built the current state of the viewmodels will be provided.
 */
fun <A : BaseMvRxViewModel<B>, B : MvRxState, C : BaseMvRxViewModel<D>, D : MvRxState> BaseEpoxyFragment.simpleController(
    viewModel1: A,
    viewModel2: C,
    buildModels: EpoxyController.(state1: B, state2: D) -> Unit
) = MvRxEpoxyController {
    if (view == null || isRemoving) return@MvRxEpoxyController
    withState(viewModel1, viewModel2) { state1, state2 ->
        buildModels(state1, state2)
    }
}

/**
 * Create a [MvRxEpoxyController] that builds models with the given callback.
 * When models are built the current state of the viewmodels will be provided.
 */
fun <A : BaseMvRxViewModel<B>, B : MvRxState, C : BaseMvRxViewModel<D>, D : MvRxState, E : BaseMvRxViewModel<F>, F : MvRxState> BaseEpoxyFragment.simpleController(
    viewModel1: A,
    viewModel2: C,
    viewModel3: E,
    buildModels: EpoxyController.(state1: B, state2: D, state3: F) -> Unit
) = MvRxEpoxyController {
    if (view == null || isRemoving) return@MvRxEpoxyController
    withState(viewModel1, viewModel2, viewModel3) { state1, state2, state3 ->
        buildModels(state1, state2, state3)
    }
}

@jQrgen
Copy link
Author

jQrgen commented Apr 10, 2019

Maybe an update to 1.0.0 will mend this issue?

The problem is that i cannot reproduce so i have to upload a new apk to my customer for quality assurance ¯_(ツ)_/¯

@jQrgen
Copy link
Author

jQrgen commented Apr 26, 2019

Solved by implementing the following:

abstract class BaseMvRxActivity : AppCompatActivity(), MvRxViewModelStoreOwner {

    /**
     * MvRx has its own wrapped ViewModelStore that enables improved state restoration if Android
     * kills your app and restores it in a new process.
     */
    override val mvrxViewModelStore by lazy { MvRxViewModelStore(viewModelStore) }

    override fun onCreate(savedInstanceState: Bundle?) {
        /**
         * This MUST be called before super!
         * In a new process, super.onCreate will trigger Fragment.onCreate, which could access a ViewModel.
         */
        mvrxViewModelStore.restoreViewModels(this, savedInstanceState)
        super.onCreate(savedInstanceState)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mvrxViewModelStore.saveViewModels(outState)
    }
}

class MainActivity: BaseMvRxActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
        if(savedInstanceState != null)
            mvrxViewModelStore.saveViewModels(savedInstanceState)
        super.onCreate(savedInstanceState)

    // ....
}

@jQrgen jQrgen closed this as completed Apr 26, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants