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 :
Et voici pour le deuxième appel:
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
}
}
}
4 Réponses :
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();
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
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"
}
}
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
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)
viewModel.getSearchResults (). distinctUntilChanged (). observe (viewLifecycleOwner, searchResultObserver)
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.
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?
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!
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
onCreatedu fragment est appelée deux fois parce que l'activité parent est mise en pause, ce qui signifie que la méthodeonCreatede 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 uncontainerViewIdexistant tel que défini dansMainActivitycontentView, ou (C) définissez un nouveaucontainerViewIddansMainActivity< hiérarchie des vues de mise en page code> contentView .@apelsoczi J'ai essayé ça maintenant. Il semble que cela soit lié au
SearhViewou auSearchManagerpuisque même lorsque vous utilisez des transactions de fragment dans MainActivity, le fragment est créé deux fois