【问题标题】:Android fragment onCreate called twiceAndroid片段onCreate调用了两次
【发布时间】:2020-07-06 13:18:52
【问题描述】:

在我的应用中,我有两个活动。在Appbar 中只有一个搜索按钮的主要活动和第二个可搜索活动。第二个活动包含一个片段,用于获取在其onCreate 调用中搜索到的数据。我的问题是片段两次获取数据。检查我的活动的生命周期,我得出结论,可搜索活动在某个时间点暂停,这显然决定了要重新创建的片段。但我不知道是什么导致活动暂停。

这是我的活动

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


}

如您所见,我使用内置的SearchManger 来处理我的搜索操作和活动之间的切换。我在文档中没有看到任何地方在搜索期间,我的可搜索活动可能会暂停或类似的事情。有谁知道为什么会这样?提前致谢!

编辑:这是我的onCreate 方法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())

    }

这里,searchResultListViewAdapterRecyclerView 的适配器,searchResult 是保存搜索结果的视图模型中的实时数据

这是onCreate()SearchFragment 的第一次调用的堆栈跟踪:

这是第二次调用:

这是ViewModelSearchFragment

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

    }
}

自从发布此应用程序后,我对应用程序进行了一些更改,现在由于某种原因在主分支中搜索不起作用。这个ViewModel 来自一个离我发布这个时间更近的分支。这是当前的ViewModel,尽管此变体中也存在问题:

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

                }
            }
    }
}

searchBook 函数只是对 API 执行 HTTP 请求,所有数据操作都在 viewModel 中处理

【问题讨论】:

  • SearchFragment 加载两次?
  • 是的,但这是因为 SearchActivity 在某个时间点暂停并重新开始
  • @AdrianPascu 如何启动 SearchActivity?
  • @dreamfire 我在应用栏中有一个 SearchView,它使用 SearchManager 为活动加注星标
  • 我认为您可能会创建两次 getSearchResults LiveData 对象。您能否发布关于如何设置值的 sn-p?

标签: android kotlin


【解决方案1】:

试试这个方法

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

【讨论】:

  • 是给我的fargment的吗?我已经在保存片段的活动中执行检查
  • 这样做会引发此错误:java.lang.RuntimeException:无法启动活动 ComponentInfo{com.adi_random.tracky/com.adi_random.tracky.SearchActivity}:java.lang.IllegalStateException:此事务是已经被添加到后台堆栈
【解决方案2】:

如果您的活动在两者之间暂停,那么您的活动的onCreate 也不应该被调用,这就是您实例化片段的地方。即片段不会再次创建(可能会再次创建视图)。

由于您在 Fragment 的 onCreate 中订阅了实时数据,因此它也不应该再次触发更新(onChanged() 不会被 liveData 调用)。

为了确保实时数据没有调用onChanged(),请在下面再次尝试(我觉得这是罪魁祸首,因为我看不到任何其他更新发生)

  • 由于您不希望再次将相同的结果发送到您的搜索页面,因此 distinctUntilChanged 可以很好地检查您的情况。

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

  • 订阅onActivityCreated的实时数据 片段。(reference)

您可以使用 viewModelScope 并从 ViewModel 内部启动,而不是使用 globalScope。(只是对干净代码的建议)

SearchFragmentLifecycleObserver 是什么?

P.S - 如果您可以分享 ViewModel 代码以及搜索回调如何触发数据,那就太好了。但是当前生命周期不应影响新片段的创建。

【讨论】:

  • 尝试了这两个想法,仍然是 2 个电话
  • SearchFragmentLifecycleObserver 是一个生命周期观察者,它只记录片段的状态。想看看片段命中的所有阶段。我只是忘了删除它。
  • 如果上述方法不起作用,则使用搜索视图启动活动时存在问题。 stackoverflow.com/a/32102128/7972699 这可能会帮助您以正确的方式设置 searchView。据我所知,您身边几乎没有遗漏。
  • 我按照该帖子中的答案设置了我的活动。我所做的唯一修改是将 MainActivity launchMode 设置为 singleTop 并创建新的 ComponentName。但问题仍然存在。你看到我这边有什么遗漏了吗?
【解决方案3】:

在你的ViewModel中使用SaveStateHandle来持久化加载的数据,不要使用GlobalContext来做抓取,把抓取封装在VieModel中。 GlobalContext 应该只用于触发和忘记操作,它不受任何视图或生命周期的约束。

您的 SearchViewModel 的样子:

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

在你的搜索片段中创建

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

    }

【讨论】:

  • 已添加。但我认为你是对的。还没有意识到我可以比较娱乐前后片段的状态。谢谢!
  • 我应该在那种状态下只检查对搜索函数的调用,还是 onCreate 中的整个代码块(包括观察者的创建和对 liveData.observe 的调用)?
  • 我使用 GlobalScope 以暂停搜索功能开始。我不知道 ViewModel 提供了它自己的协程范围,所以谢谢你。我实现了您的代码,但 onCreate 中的搜索功能仍然被调用两次。我可以做些什么来更好地查明为什么 onCreate get 被调用了两次?我认为更多信息会有所帮助
  • hmmmm,你能在 onCreate 中放置一个断点并发布堆栈跟踪吗?所以我们可以看到从外部调用 onCreate 的原因以及导致它的原因。
  • 好的,从看到你的堆栈跟踪,我不知道为什么 onCreated 被调用了两次。为了找出原因,我必须自己调试代码......现在如果你有一个变量来阻止代码加载搜索结果两次就足够了,我在我的答案中更新了代码。
【解决方案4】:

我认为负责文档的 Android 团队确实应该做得更好。我继续前进,只是从SearchView 中删除了SearchManager,并直接使用onQueryTextListener,结果发现通过这种方法,我的监听器也被调用了两次。但是感谢this 的帖子,我看到这显然是模拟器的一个错误(或者SearchView 处理提交事件的方式)。因此,如果我按下 OSK 输入按钮,一切都会按预期工作。

感谢大家的帮助!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多