Android - View instance gets null on screen rotati

2019-07-07 01:25发布

问题:

I am using Kotlin Android Extension to access view directly by their id. I have a progress bar which I access directly in fragment using id i.e progress_bar

<ProgressBar
    android:id="@+id/progress_bar"
    style="@style/Widget.AppCompat.ProgressBar.Horizontal"
    android:layout_width="match_parent"
    android:layout_height="15dp"
    android:indeterminate="true"/>

In fragment, I am showing and hiding it with this code

progress_bar.visibility = if (visible) View.VISIBLE else View.GONE

It is working perfectly until I rotate the screen. After that, it throws the exception

java.lang.IllegalStateException: progress_bar must not be null.

The variable gets null on screen rotation. How to solve this problem?

Fragment code

class SingleAppFragment : Fragment() {

private lateinit var appName: String

companion object {
    fun newInstance(appName: String = ""): SingleAppFragment {
        val fragment = SingleAppFragment()
        val args = Bundle()
        args.putString(Constants.EXTRA_APP_NAME, appName)
        fragment.arguments = args
        return fragment
    }
}

private var mListener: OnFragmentInteractionListener? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    appName = if (arguments != null && !arguments.getString(Constants.EXTRA_APP_NAME).isEmpty()) {
        arguments.getString(Constants.EXTRA_APP_NAME)
    } else {
        Constants.APP_NAME_FACEBOOK
    }
}

override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    return inflater!!.inflate(R.layout.fragment_single_app, container, false)
}

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

private fun initView() {
    var canShowSnackBar = true

    web_single_app.webViewClient = object : WebViewClient() {

        override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
            super.onPageStarted(view, url, favicon)
            showHideProgressBar(true)
            canShowSnackBar = true
        }

        override fun onPageFinished(view: WebView?, url: String?) {
            super.onPageFinished(view, url)
            showHideProgressBar(false)
        }

        override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
            web_single_app.stopLoading()
            if (canShowSnackBar) {
                mListener?.onErrorWebView()
                canShowSnackBar = false
            }
        }
    }
    web_single_app.settings.javaScriptEnabled = true
    web_single_app.loadUrl(Constants.APP_NAME_URL_MAP[appName])
}

private fun setEventListeners() {
    back_web_control.setOnClickListener({
        web_single_app.goBack()
    })
}

fun showHideProgressBar(visible: Boolean) {
    progress_bar_web_control.visibility = if (visible) View.VISIBLE else View.GONE
}

fun loadUrl(appName: String) {
    web_single_app.loadUrl(Constants.APP_NAME_URL_MAP[appName])
}

override fun onAttach(context: Context?) {
    super.onAttach(context)
    if (context is OnFragmentInteractionListener) {
        mListener = context
    }
}

override fun onDetach() {
    super.onDetach()
    mListener = null
}

interface OnFragmentInteractionListener {
    fun onErrorWebView()
}
}

Steps to reproduce:

  1. Start Activity
  2. Fragment get loaded
  3. At Fragment load, I load an URL and show a progress bar
  4. At loading the URL I rotate the phone and the progress bar variable gets null

回答1:

In my case this bug happens from time to time. Of course, onViewCreated() is a good method to place your code in. But sometimes it's strangely not enough. And setRetainInstance(true) may help, may not. So sometimes this helps: access your Views with a view variable. You can even access them inside onCreateView(). You can use ?. for a guarantee that an application won't crash (of course, some views won't update in this case). If you wish to get context, use view.context.

In my case this bug reproduced only in Kotlin coroutines.

private fun showProgress(view: View) {
    view.progressBar?.visibility = View.VISIBLE
}

private fun hideProgress(view: View) {
    view.progressBar?.visibility = View.GONE
}

Then in code:

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

    showData(view)
}

private fun showData(view: View) {
    showProgress(view)
    adapter = SomeAdapter()
    adapter.setItems(items)
    val divider = SomeItemDecoration(view.context)
    view.recycler_view?.run {
        addItemDecoration(divider)
        adapter = this@SomeFragment.adapter
        layoutManager = LinearLayoutManager(view.context)
        setHasFixedSize(true)
    }
    hideProgress(view)
}


回答2:

In which method do you get the progress_bar by Id?

Please consider the fragment state lifecycle. Maybe you try to load it when the view is not ready yet.

Ensure your progress_bar variable is assigned only after the view is ready. For example in the onViewCreated method.

See here the official Android lifecycle:

Update

As @CoolMind pointed out the diagram doesn't show the method onViewCreated.

The complete Android Activity/Fragment lifecycle can be found here:



回答3:

Add retain intance true to the fragment so that it will not be destroyed when an orientation changes occurs

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    retainInstance=true
}

Also do a null check using safe call operator before accessing views

fun showHideProgressBar(visible: Boolean) {
    progress_bar_web_control?.visibility = if (visible) View.VISIBLE else View.GONE
}