0

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 RecyclerViewand 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: enter image description here

And here is for the second call: enter image description here

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

Adrian Pascu
  • 949
  • 5
  • 20
  • 48
  • SearchFragment s loading two times? – Shalu T D Jul 06 '20 at 13:50
  • Yes, but this is because the SearchActivity get's paused at some point and starts again – Adrian Pascu Jul 07 '20 at 11:39
  • @AdrianPascu How do you start the SearchActivity? – dreamfire Jul 10 '20 at 08:16
  • @dreamfire I have a SearchView in the appbar that stars the activity using the SearchManager – Adrian Pascu Jul 10 '20 at 16:32
  • I think that you may be creating the getSearchResults LiveData object twice. Could you post the snippet on how you set the value? – Eliza Camber Jul 14 '20 at 16:06
  • @AdrianPascu are by chance you able to upload gist which is having all complete object references? showing complete possibility for error? ie: how is Activity being registered in manifest.xml launched/started. From reading I am thinking and believing activity is doing exactly as needs to be and error is being some unknowns and complexity you've kicked up from not knowing enough about Activity Stacks and Tasks. Recommendation is try and implement SearchFragment into same place as SearchView and forget second activity. – apelsoczi Jul 15 '20 at 03:16
  • @apelsoczi As I said, the fragment's `onCreate` method gets called twice because the parent activity gets paused which in term means the parent activity's `onCreate` method gets called twice. How would moving the fragment logic into the activity change anything? – Adrian Pascu Jul 15 '20 at 07:39
  • @AdrianPascu if you know that second activity 'onCreate' is being called twice and it shouldn't be. Why are you attempting to use second activity implementation? All that is required to display SearchFragment is a `containerViewId`. You have options of **(A)** maintaining double broken activity configuration, **(B)** using single activity and perform fragment transaction on an existing `containerViewId` as defined in `MainActivity` `contentView`, or **(C)** define a new `containerViewId` in `MainActivity` `contentView` layout view hierarchy. – apelsoczi Jul 19 '20 at 00:46
  • @apelsoczi I tried that now. It seems like it's related to the `SearhView` or `SearchManager` since even when using fragment transactions in the MainActivity, the fragment it's created twice – Adrian Pascu Jul 19 '20 at 09:03

4 Answers4

0

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();
hossam rakha
  • 213
  • 6
  • 13
  • Is that for my fargment? I am already performing the check in the activity that holds the fragment – Adrian Pascu Jul 06 '20 at 13:28
  • Doing this trows this error: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.adi_random.tracky/com.adi_random.tracky.SearchActivity}: java.lang.IllegalStateException: This transaction is already being added to the back stack – Adrian Pascu Jul 06 '20 at 15:03
0

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()

    }
Minki
  • 934
  • 5
  • 14
  • Added it. But I think you are right. Haven't realized I can compare the state of the fragment before and after the recreation. Thx! – Adrian Pascu Jul 11 '20 at 14:59
  • Should I wrap in that state check only the call to the search function, or the entire code block inside onCreate (including the creation of the Observer and the call to liveData.observe)? – Adrian Pascu Jul 11 '20 at 15:02
  • I used GlobalScope to start by suspendig search function. I didn't know ViewModel provided a coroutine scope of it's own, so thanks for that. I implemented your code, but the search function in onCreate still gets called twice. Is there something I can do to better pinpoint why onCreate get's called twice? I assume more information could help – Adrian Pascu Jul 13 '20 at 08:05
  • hmmmm, could you put a breakpoint in onCreate and post the stacktrace. So we can see what is calling onCreate from outside and what is causing it. – Minki Jul 13 '20 at 08:12
  • Okay, from seeing your stack trace, I have no idea why the onCreated is called twice. For finding out the reason, I have to debug the code myself... For now it is enough if you have a variable to hinder the code to load the search result twice, I updated the code in my answer. – Minki Jul 16 '20 at 15:11
  • I tried it, but with no success. The viewmodel never gets to have its state saved, since both times the search is called, it seems to be a new viewmodel instance, since even though the isLoading gets to the point where it is set to true in the first call, in the second call it's false – Adrian Pascu Jul 18 '20 at 12:12
0

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.i.e 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?

P.S - 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.

Anmol
  • 8,110
  • 9
  • 38
  • 63
  • Tried both ideas, still 2 calls – Adrian Pascu Jul 14 '20 at 20:31
  • `SearchFragmentLifecycleObserver` is a lifecycle observer that just logs the state of the fragment. Wanted to see all the stages the fragment hits. I just forgot to delete it. – Adrian Pascu Jul 14 '20 at 20:37
  • If the above is not working then there is a issue in starting the activity using search view. https://stackoverflow.com/a/32102128/7972699 this might help you to setup the searchView in the right way.From what i can see there are few misses in your side. – Anmol Jul 15 '20 at 06:18
  • I set up my activities as the answer in that post said. The only modifications I made was setting the MainActivity launchMode to singleTop and create that new ComponentName. But the issue is still here. What misses on my side did you see? – Adrian Pascu Jul 15 '20 at 07:54
0

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 SearchViewand 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!

Adrian Pascu
  • 949
  • 5
  • 20
  • 48