0
votes

Fragment Android onCreate appelé deux fois

Dans mon application, j'ai deux activités. L'activité principale qui n'a qu'un bouton de recherche dans la Appbar et une deuxième activité, consultable. La deuxième activité contient un fragment qui récupère les données recherchées dans son appel onCreate . Mon problème est que le fragment récupère les données deux fois. En inspectant le cycle de vie de mes activités, j'ai conclu que l'activité de recherche est interrompue à un moment donné, ce qui détermine évidemment le fragment à recréer. Mais je n'ai aucune idée de la cause de la suspension de l'activité.

Voici mes activités

MainActivity.kt

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

                }
            }
    }
}

SearchActivity.kt p>

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

    }
}

Comme vous pouvez le voir, j'utilise le SearchManger intégré pour gérer mon action de recherche et basculer entre les activités. Je n'ai vu nulle part dans la documentation que pendant la recherche, mon activité de recherche pourrait être interrompue ou quelque chose du genre. Quelqu'un at-il une idée de pourquoi cela se produit? Merci d'avance!

edit: Voici ma méthode onCreate pour le 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())

    }

Ici, searchResultListViewAdapter est l'adaptateur pour un RecyclerView et searchResult est une donnée dans le modèle de vue contenant le résultat de la recherche

Voici la trace de la pile pour le premier appel de onCreate () sur SearchFragment : entrez la description de l'image ici

Et voici pour le deuxième appel: entrez la description de l'image ici

Voici le ViewModel pour le SearchFragment:

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
        }
    }


}


9 commentaires

SearchFragment se charge deux fois?


Oui, mais c'est parce que SearchActivity est mis en pause à un moment donné et recommence


@AdrianPascu Comment démarrer la SearchActivity?


@dreamfire J'ai un SearchView dans la barre d'applications qui met en vedette l'activité à l'aide du SearchManager


Je pense que vous créez peut-être l'objet getSearchResults LiveData deux fois. Pourriez-vous publier l'extrait de code sur la façon dont vous définissez la valeur?


@AdrianPascu êtes-vous par hasard capable de télécharger l'essentiel qui contient toutes les références d'objets complètes? montrant une possibilité d'erreur complète? c'est-à-dire: comment l'activité est-elle enregistrée dans manifest.xml lancée / démarrée. D'après la lecture, je pense et je crois que l'activité se déroule exactement comme il se doit et que l'erreur est le fait d'inconnues et de complexité que vous avez soulevées en ne sachant pas suffisamment sur les piles d'activités et les tâches. La recommandation est d'essayer d'implémenter SearchFragment au même endroit que SearchView et d'oublier la deuxième activité.


@apelsoczi Comme je l'ai dit, la méthode onCreate du fragment est appelée deux fois parce que l'activité parent est mise en pause, ce qui signifie que la méthode onCreate de l'activité parent est appelée deux fois. En quoi le fait de déplacer la logique du fragment dans l'activité changerait-il quoi que ce soit?


@AdrianPascu si vous savez que la deuxième activité 'onCreate' est appelée deux fois et qu'elle ne devrait pas l'être. Pourquoi essayez-vous d'utiliser la mise en œuvre de la deuxième activité? Tout ce qui est nécessaire pour afficher SearchFragment est un containerViewId . Vous avez les options de (A) maintenir la configuration d’activité double interrompue, (B) d’utiliser une seule activité et d’effectuer une transaction fragmentée sur un containerViewId existant tel que défini dans MainActivity contentView , ou (C) définissez un nouveau containerViewId dans MainActivity < hiérarchie des vues de mise en page code> contentView .


@apelsoczi J'ai essayé ça maintenant. Il semble que cela soit lié au SearhView ou au SearchManager puisque même lorsque vous utilisez des transactions de fragment dans MainActivity, le fragment est créé deux fois


4 Réponses :


0
votes

essayez de cette façon

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


2 commentaires

Est-ce pour mon jugement? J'effectue déjà la vérification dans l'activité qui contient le fragment


Faire ceci trows cette erreur: java.lang.RuntimeException: Impossible de démarrer l'activité ComponentInfo {com.adi_random.tracky / com.adi_random.tracky.Se‌ archActivity}: java.lang.IllegalStateException: Cette transaction est déjà ajoutée à l'arrière empiler



0
votes

Utilisez SaveStateHandle dans votre ViewModel pour conserver les données chargées, et n'utilisez pas GlobalContext pour effectuer la récupération, encapsulez la récupération dans VieModel. GlobalContext ne doit être utilisé que pour les actions d'incendie et d'oubli, qui ne sont liées ni aux vues ni au cycle de vie.

À quoi pourrait ressembler votre SearchViewModel:

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

    }

Et dans votre recherche Fragment onCreate

@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"
    }
}


7 commentaires

Cela ne répond pas à la question. Une fois que vous aurez une réputation suffisante, vous pourrez commenter n'importe quel message ; à la place, fournit des réponses qui ne nécessitent pas de clarification de la part du demandeur . - De l'avis


L'ajouté. Mais je pense que tu as raison. Je n'ai pas réalisé que je pouvais comparer l'état du fragment avant et après la récréation. Merci!


Dois-je encapsuler dans cet état vérifier uniquement l'appel à la fonction de recherche, ou tout le bloc de code dans onCreate (y compris la création de l'Observer et l'appel à liveData.observe)?


J'ai utilisé GlobalScope pour démarrer par la fonction de recherche suspendig. Je ne savais pas que ViewModel fournissait sa propre portée de coroutine, alors merci pour cela. J'ai implémenté votre code, mais la fonction de recherche dans onCreate est toujours appelée deux fois. Y a-t-il quelque chose que je puisse faire pour mieux cerner pourquoi onCreate est appelé deux fois? Je suppose que plus d'informations pourraient vous aider


hmmmm, pourriez-vous mettre un point d'arrêt dans onCreate et publier le stacktrace. Ainsi, nous pouvons voir ce qui appelle onCreate de l'extérieur et ce qui le cause.


D'accord, en voyant votre trace de pile, je n'ai aucune idée de la raison pour laquelle onCreated est appelé deux fois. Pour découvrir la raison, je dois déboguer le code moi-même ... Pour l'instant il suffit que vous ayez une variable pour gêner le code pour charger le résultat de la recherche deux fois, j'ai mis à jour le code dans ma réponse.


Je l'ai essayé, mais sans succès. Le viewmodel ne parvient jamais à avoir son état sauvegardé, puisque les deux fois que la recherche est appelée, il semble être une nouvelle instance de viewmodel, car même si isLoading arrive au point où il est défini sur true dans le premier appel, dans le second appeler c'est faux



0
votes

Si votre activité est mise en pause entre les deux, alors onCreate de votre activité ne doit pas être appelée et c'est là que vous instanciez le fragment.ie Le fragment n'est pas créé à nouveau (la vue peut être créée à nouveau) .

Comme vous vous êtes abonné aux données en direct dans onCreate de Fragment, il ne devrait pas non plus déclencher de mise à jour ( onChanged () ne sera plus appelé pour liveData ).

/ p>

Juste pour être sûr que les données en direct n'appellent pas onChanged () , réessayez ci-dessous (je pense que c'est le coupable ici car je ne vois aucune autre mise à jour se produire)

  • Comme vous ne voudrez plus renvoyer le même résultat à votre page de recherche, distinctUntilChanged est une bonne vérification pour votre cas.

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

  • Abonnez-vous aux données en direct dans onActivityCreated sur fragment. ( référence )

Au lieu d'utiliser globalScope, vous pouvez utiliser viewModelScope et lancer depuis l'intérieur de votre ViewModel. (juste une suggestion de code propre)

Et qu'est-ce que SearchFragmentLifecycleObserver ?

P.S - Si vous pouvez partager le code ViewModel et comment les rappels de recherche déclenchent des données, ce sera génial.Mais le cycle de vie actuel ne devrait pas affecter la création d'un nouveau fragment.


4 commentaires

J'ai essayé les deux idées, toujours 2 appels


SearchFragmentLifecycleObserver est un observateur de cycle de vie qui enregistre simplement l'état du fragment. Je voulais voir toutes les étapes que le fragment atteint. J'ai juste oublié de le supprimer.


Si ce qui précède ne fonctionne pas, il y a un problème lors du démarrage de l'activité à l'aide de la vue de recherche. stackoverflow.com/a/32102128/7972699 cela peut vous aider à configurer le searchView de la bonne manière. voyez, il y a quelques ratés de votre côté.


J'ai mis en place mes activités comme le disait la réponse dans ce post. Les seules modifications que j'ai apportées ont été de définir MainActivity launchMode sur singleTop et de créer ce nouveau ComponentName. Mais le problème est toujours là. Qu'est-ce qui manque de mon côté avez-vous vu?



0
votes

Je pense que l'équipe Android en charge de la documentation devrait vraiment faire un meilleur travail. Je suis allé de l'avant et je viens de supprimer le SearchManager de SearchView et d'utiliser le onQueryTextListener directement, seulement pour voir qu'avec cette approche, j'obtiens également mon auditeur appelé deux fois. Mais grâce à ce message , j'ai vu qu'apparemment, c'est un bogue avec l'émulateur (ou avec la façon dont SearchView gère l'événement de soumission). Donc, si j'appuie sur le bouton d'entrée OSK, tout fonctionne comme prévu.

Merci à tous pour leur aide!


0 commentaires