简体   繁体   中英

Android fragment onCreate called twice

In my app I have two activities. The main activity that only has a search button in the Appbar and a second, searchable, activity. The second activity hold a fragment that fetches the data searched in it's onCreate call. My problem is that the fragment fetches the data twice. Inspecting the lifecycle of my activities, I concluded that the searchable activity gets paused at some point, which obviously determines the fragment to be recreated. But I have no idea what causes the activity to be paused.

Here are my activities

MainActivity.kt

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val root = binding.root
        setContentView(root)
        //Setup the app bar
        setSupportActionBar(binding.toolbar);

    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        return initOptionMenu(menu, this)
    }

}


fun initOptionMenu(menu: Menu?, context: AppCompatActivity): Boolean {
    val inflater = context.menuInflater;
    inflater.inflate(R.menu.app_bar_menu, menu)

    // Get the SearchView and set the searchable configuration
    val searchManager = context.getSystemService(Context.SEARCH_SERVICE) as SearchManager
    (menu?.findItem(R.id.app_bar_search)?.actionView as SearchView).apply {
        // Assumes current activity is the searchable activity
        setSearchableInfo(searchManager.getSearchableInfo(context.componentName))
        setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
    }

    return true;
}

SearchActivity.kt

class SearchActivity : AppCompatActivity() {

    private lateinit var viewBinding: SearchActivityBinding
    private var query: String? = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = SearchActivityBinding.inflate(layoutInflater)
        val root = viewBinding.root
        setContentView(root)


        // Setup app bar

        supportActionBar?.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
        supportActionBar?.setCustomView(R.layout.search_app_bar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)

        //Get the query string
        if (Intent.ACTION_SEARCH == intent.action) {
            intent.getStringExtra(SearchManager.QUERY).also {

                //Add the query to the appbar
                query = it
                updateAppBarQuery(it)
            }
        }

        //Instantiate the fragment
        if (savedInstanceState == null) {
            val fragment = SearchFragment.newInstance();
            val bundle = Bundle();
            bundle.putString(Intent.ACTION_SEARCH, query)
            fragment.arguments = bundle;
            supportFragmentManager.beginTransaction()
                .replace(R.id.container, fragment)
                .commitNow()
        }


    }

    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        return initOptionMenu(menu, this)
    }


    private fun updateAppBarQuery(q: String?) {
        supportActionBar?.customView?.findViewById<TextView>(R.id.query)?.apply {
            text = q
        }
    }


}

As you can see, I am using the built in SearchManger to handle my search action and switching between activities. I haven't seen anywhere in the docs that during search, my searchable activity might get paused or anything like that. Does anyone have any idea why this happens? Thanks in advance!

edit: Here is my onCreate method for the SearchFragment :

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val query = arguments?.getString(Intent.ACTION_SEARCH);

        //Create observers

        val searchResultObserver = Observer<Array<GoodreadsBook>> {
            searchResultListViewAdapter.setData(it)
        }
        viewModel.getSearchResults().observe(this, searchResultObserver)


        GlobalScope.launch {  //Perform the search
            viewModel.search(query)
        }


        lifecycle.addObserver(SearchFragmentLifecycleObserver())

    }

Here, searchResultListViewAdapter is the adapter for a RecyclerView and searchResult is a livedata in the view-model holding the search result

Here is the stack trace for the first call of onCreate() on SearchFragment : 在此处输入图像描述

And here is for the second call: 在此处输入图像描述

Here is the ViewModel for the SearchFragment :

class SearchViewModel() : ViewModel() {
    private val searchResults: MutableLiveData<Array<GoodreadsBook>> by lazy {
        MutableLiveData<Array<GoodreadsBook>>();
    }

    fun getSearchResults(): LiveData<Array<GoodreadsBook>> {
        return searchResults;
    }

    //    TODO: Add pagination
    suspend fun search(query: String?) = withContext(Dispatchers.Default) {
        val callback: Callback = object : Callback {
            override fun onFailure(call: Call, e: IOException) {
//                TODO: Display error message
            }

            override fun onResponse(call: Call, response: Response) {
                //                TODO: Check res status

                val gson = Gson();
                val parsedRes = gson.fromJson(
                    response.body?.charStream(),
                    Array<GoodreadsBook>::class.java
                );
                // Create the bitmap from the imageUrl
                searchResults.postValue(parsedRes)
            }


        }
        launch { searchBook(query, callback) }

    }
}

I made some changes to the app since posted this and right now the search doesn't work for some reason in the main branch. This ViewModel it's from a branch closer to the time I posted this. Here is the current ViewModel , although the problem is present in this variant as well:

class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
//    private val searchResults: MutableLiveData<Array<GoodreadsBook>> by lazy {
////        MutableLiveData<Array<GoodreadsBook>>();
////    }

    companion object {
        private const val SEARCH_RESULTS = "searchResults"
    }

    fun getSearchResults(): LiveData<Array<GoodreadsBook>> =
        savedStateHandle.getLiveData<Array<GoodreadsBook>>(SEARCH_RESULTS)


    //    TODO: Add pagination
    fun search(query: String?) {
        val searchResults = savedStateHandle.getLiveData<Array<GoodreadsBook>>(SEARCH_RESULTS)
        if (searchResults.value == null)
            viewModelScope.launch {
                withContext(Dispatchers.Default) {
                    //Handle the API response
                    val callback: Callback = object : Callback {
                        override fun onFailure(call: Call, e: IOException) {
//                TODO: Display error message
                        }

                        override fun onResponse(call: Call, response: Response) {
                            //                TODO: Check res status

                            val gson = Gson();
                            val parsedRes = gson.fromJson(
                                response.body?.charStream(),
                                Array<GoodreadsBook>::class.java
                            );

                            searchResults.postValue(parsedRes)
                        }


                    }
                    launch { searchBook(query, callback) }

                }
            }
    }
}

The searchBook function just performs the HTTP request to the API, all the data manipulation is handled in the viewModel

try this way

    Fragment sf = SearchFragment.newInstance();
    Bundle args = new Bundle();
    args.putString(Intent.ACTION_SEARCH, query);
    sf.setArguments(args);

    getFragmentManager().beginTransaction()
            .replace(R.id.fragmentContainer, sf).addToBackStack(null).commit();

Use SaveStateHandle in your ViewModel to persist the loaded data, and don't use GlobalContext to do the fetching, encapsulate the fetching in VieModel. GlobalContext should only be used for fire and forget actions, which are not bound the any views or lifecycle.

How your SearchViewModel could look like:

@Parcelize
class SearchResult(
        //fields ...
) : Parcelable

class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    private var isLoading : Boolean = false
    
    fun searchLiveData() : LiveData<SearchResult> = savedStateHandle.getLiveData<SearchResult>(EXTRA_SEARCH)

    fun fetchSearchResultIfNotLoaded() { //do this in onCreate
        val liveData = savedStateHandle.getLiveData<SearchResult>(EXTRA_SEARCH)
        if(liveData.value == null) {
            if(isLoading) 
                return
            
            isLoading = true
            viewModelScope.launch {
                try {
                    val result = withContext(Dispatchers.IO) {
                        //fetching task
                        SearchResult()
                    }
                    liveData.value = result
                    isLoading = false
                }catch (e : Exception) {
                    //log
                    isLoading = false
                }
            }
        }
    }

    companion object {
        private const val EXTRA_SEARCH = "EXTRA_SEARCH"
    }
}

And in your Search Fragment onCreate

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        val searchResultObserver = Observer<Array<GoodreadsBook>> {
            searchResultListViewAdapter.setData(it)
        }
        viewModel.searchLiveData().observe(viewLifeCycleScope, searchResultObserver)
        viewModel.fetchSearchResultIfNotLoaded()

    }

If your activity is getting paused in between then also onCreate of your activity should not be called and that's where you are instantiating the fragment.ie Fragment is not created again(view might be created again).

As as you have subscribed live data in onCreate of Fragment it should also not trigger an update( onChanged() won't be called for liveData ) again.

Just to be sure about live data is not calling onChanged() again try below (i feel that's the culprit here as i can't see any other update happening)

  • As you will not want to send the same result to your search page again so distinctUntilChanged is a good check for your case.

viewModel.getSearchResults().distinctUntilChanged().observe(viewLifecycleOwner, searchResultObserver)

  • Do subscription of live data in onActivityCreated of fragment.( reference )

Instead of using globalScope you can use viewModelScope and launch from inside your ViewModel.(just a suggestion for clean code)

And what's SearchFragmentLifecycleObserver ?

PS - If you can share the ViewModel code and how the search callback's are triggering data it will be great.But Current lifecycle should not effect the creation of new fragment.

I think the Android team in charge of the documentation should really do a better job. I went ahead and just removed the SearchManager from the SearchView and use the onQueryTextListener directly, only to see that with this approach I also get my listener called twice. But thanks to this post, I saw that apparently it's a bug with the emulator (or with the way SearchView handles the submit event). So if I press the OSK enter button everything works as expected.

Thanks everyone for their help!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM