Kotlin Android Activity创建时数据不显示,需二次加载问题求助
Hey Andrea, let's break down why this is happening and how to fix it properly:
The Root Problem
Your Volley network requests are asynchronous—they run in the background while your Activity and Fragments finish initializing. When the requests complete and populate those Companion Object global variables, your Fragments have already finished inflating their UI and won't automatically refresh. Restarting the Activity works because the Fragments re-read those global variables during their next initialization.
Plus, using Companion Object for shared data is risky: it's not lifecycle-aware, can cause memory leaks, and leads to inconsistent state across configuration changes (like rotating the screen).
The Fix: Use ViewModel + LiveData
We'll refactor your code to follow the MVVM pattern, which ensures data updates automatically trigger UI refreshes without needing to restart the Activity.
Step 1: Create a Shared ViewModel
This ViewModel will handle all network requests and store data using LiveData, which notifies Fragments when data changes.
import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.android.volley.Request import com.android.volley.Response import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.Volley import org.json.JSONObject class SharedViewModel : ViewModel() { // Traffic data (mutable for internal updates, immutable for observers) private val _trafficData = MutableLiveData<TrafficData>() val trafficData: LiveData<TrafficData> = _trafficData // IP Blocked data private val _ipBlockedData = MutableLiveData<IpBlockedData>() val ipBlockedData: LiveData<IpBlockedData> = _ipBlockedData // Search Terms data private val _searchTermsData = MutableLiveData<SearchTermsData>() val searchTermsData: LiveData<SearchTermsData> = _searchTermsData // Fetch Traffic Dashboard data fun fetchTrafficData(context: android.content.Context, baseUrl: String, token: String, filter: String?) { val queue = Volley.newRequestQueue(context) val requestUrl = "$baseUrl/homeApp?token=$token&filter=${filter ?: ""}" val stringRequest = StringRequest( Request.Method.GET, requestUrl, Response.Listener { response -> val jsonObj = JSONObject(response) val status = jsonObj.getInt("status") val clicksArray = jsonObj.getJSONArray("clicks") val blocked = jsonObj.getInt("bloccati") val totalAdwordsClicks = jsonObj.getInt("totalClicksAdwords") val clicksList = mutableListOf<TrafficClick>() for (i in 0 until clicksArray.length()) { val item = clicksArray.getJSONObject(i) val click = TrafficClick( item.getString("id"), item.getString("ip"), item.getString("adwords"), item.getString("browser"), item.getString("keyword"), item.getString("id_campaign"), item.getString("os"), item.getString("mobile"), item.getString("guid"), item.getString("referer"), item.getString("from_referer"), item.getString("organization"), item.getString("data_creation"), item.getString("data_update"), item.getString("eliminato"), item.getString("counter"), item.getString("id_dominio"), item.getString("id_geo_ip"), item.getString("iso_code") ) if (!click.eliminato.isNullOrEmpty()) clicksList.add(click) } // Update LiveData (triggers Fragment UI refresh) _trafficData.postValue( TrafficData( status.toString(), clicksList.size.toString(), totalAdwordsClicks.toString(), blocked.toString(), clicksList ) ) }, Response.ErrorListener { _trafficData.postValue(null) // Signal error to Fragment } ) queue.add(stringRequest) } // Fetch IP Blocked data (similar pattern to Traffic) fun fetchIpBlockedData(context: android.content.Context, baseUrl: String, token: String) { val queue = Volley.newRequestQueue(context) val requestUrl = "$baseUrl/listIpBlocked?token=$token" val stringRequest = StringRequest( Request.Method.GET, requestUrl, Response.Listener { response -> val jsonObj = JSONObject(response) val status = jsonObj.getInt("status") val ipsArray = jsonObj.getJSONArray("ips") val ipsList = mutableListOf<IpListBlock>() for (i in 0 until ipsArray.length()) { val item = ipsArray.getJSONObject(i) val ip = IpListBlock( item.getInt("id"), item.getInt("id_user"), item.getInt("id_dominio"), item.getInt("id_criterion"), item.getInt("id_adwords_campaign"), item.getInt("id_adwords_customer"), item.getString("ip"), item.getString("trigger"), item.getString("created_at"), item.getString("removal_date"), item.getString("unlimited"), item.getString("updated_at"), item.getString("deleted_at"), item.getInt("id_click"), item.getString("organization"), item.getString("iso_code") ) ipsList.add(ip) } _ipBlockedData.postValue( IpBlockedData( status.toString(), ipsList.size, ipsList ) ) }, Response.ErrorListener { _ipBlockedData.postValue(null) } ) queue.add(stringRequest) } // Fetch Search Terms data (similar pattern) fun fetchSearchTermsData(context: android.content.Context, baseUrl: String, token: String, filter: String?) { val queue = Volley.newRequestQueue(context) val requestUrl = "$baseUrl/reportApp?token=$token&filter=${filter ?: ""}" val stringRequest = StringRequest( Request.Method.GET, requestUrl, Response.Listener { response -> val jsonObj = JSONObject(response) val status = jsonObj.getInt("status") val searchTermsArray = jsonObj.getJSONArray("searchTerms") val clicks = jsonObj.getInt("clicks") val blocked = jsonObj.getInt("bloccati") totalClickAdwordsSearch = jsonObj.getInt("bloccati").toString() val searchTermsList = mutableListOf<SearchTermsList>() for (i in 0 until searchTermsArray.length()) { val item = searchTermsArray.getJSONObject(i) val searchTerm = SearchTermsList( item.getInt("id"), item.getInt("id_user"), item.getInt("id_adwords_customer"), item.getInt("id_adwords_campaign"), item.getString("campaign"), item.getString("search_term"), item.getString("keyword"), item.getInt("keyword_id"), item.getInt("clicks"), item.getInt("impressions"), item.getInt("interactions"), item.getString("interactions_type"), item.getInt("conversions"), item.getInt("cost_conv"), item.getInt("total_conv"), item.getInt("cost"), item.getInt("avg_cpv"), item.getString("currency"), item.getString("device"), item.getString("date"), item.getString("created_at"), item.getString("updated_at"), item.getString("deleted_at") ) searchTermsList.add(searchTerm) } _searchTermsData.postValue( SearchTermsData( status.toString(), clicks.toString(), blocked.toString(), totalClickAdwordsSearch, searchTermsList.size, searchTermsList ) ) }, Response.ErrorListener { _searchTermsData.postValue(null) } ) queue.add(stringRequest) } // Data classes to encapsulate each screen's data data class TrafficData( val status: String, val totalClicks: String, val totalAdwordsClicks: String, val totalBlocked: String, val clicks: List<TrafficClick> ) data class IpBlockedData( val status: String, val ipsNumber: Int, val ips: List<IpListBlock> ) data class SearchTermsData( val status: String, val totalClicks: String, val totalBlocked: String, val totalAdwordsClicks: String, val searchTermsLength: Int, val searchTerms: List<SearchTermsList> ) }
Step 2: Update MainActivity to Use the ViewModel
Remove the Companion Object global variables and initialize the ViewModel to handle requests:
package com.example.example import android.content.Context import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { private lateinit var sharedViewModel: SharedViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // Initialize ViewModel sharedViewModel = ViewModelProvider(this)[SharedViewModel::class.java] // Get saved preferences val sharedPreferences = getSharedPreferences("TOKEN_FILE", Context.MODE_PRIVATE) val baseUrl = sharedPreferences.getString("URL", "") ?: "" val token = sharedPreferences.getString("TOKEN", "") ?: "" val trafficFilter = sharedPreferences.getString("changeDataTraffic", "") val searchTermsFilter = sharedPreferences.getString("changeDataSearchTerms", "") // Launch all data requests sharedViewModel.fetchTrafficData(this, baseUrl, token, trafficFilter) sharedViewModel.fetchIpBlockedData(this, baseUrl, token) sharedViewModel.fetchSearchTermsData(this, baseUrl, token, searchTermsFilter) // Setup bottom navigation val navView: BottomNavigationView = findViewById(R.id.nav_view) val navController = findNavController(R.id.nav_host_fragment) navView.setupWithNavController(navController) } }
Step 3: Update Fragments to Observe LiveData
Modify your TrafficFragment (and other Fragments) to watch for data changes and update UI automatically:
package com.example.example.ui.traffic import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import com.example.example.SharedViewModel import com.example.example.databinding.FragmentTrafficBinding class TrafficFragment : Fragment() { private var _binding: FragmentTrafficBinding? = null private val binding get() = _binding!! private lateinit var sharedViewModel: SharedViewModel private lateinit var trafficAdapter: TrafficAdapter // Replace with your actual adapter override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentTrafficBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedViewModel = ViewModelProvider(requireActivity())[SharedViewModel::class.java] // Initialize RecyclerView trafficAdapter = TrafficAdapter() binding.recyclerViewTraffic.apply { layoutManager = LinearLayoutManager(context) adapter = trafficAdapter } // Observe Traffic data updates sharedViewModel.trafficData.observe(viewLifecycleOwner) { trafficData -> trafficData?.let { // Update UI elements binding.totalClicksText.text = it.totalClicks binding.totalBlockedText.text = it.totalBlocked binding.totalAdwordsText.text = it.totalAdwordsClicks trafficAdapter.submitList(it.clicks) // Update recycler view data } ?: run { // Handle error state (e.g., show error message) binding.errorMessage.visibility = View.VISIBLE } } } override fun onDestroyView() { super.onDestroyView() _binding = null // Prevent memory leaks } }
Why This Works
- Lifecycle Awareness: The ViewModel persists across configuration changes (like screen rotation) and doesn't leak memory.
- Automatic UI Updates: LiveData notifies your Fragment whenever data changes, so you don't need to restart the Activity to see new data.
- Cleaner Architecture: Separates data fetching from UI logic, making your code easier to maintain and test.
Bonus: Refresh Data After Modifications
When you update filters or modify data, call the corresponding fetch method in the ViewModel again. For example:
// In your Fragment when a filter is changed sharedViewModel.fetchTrafficData(requireContext(), baseUrl, token, newFilterValue)
The LiveData will update, and your Fragment will refresh automatically.
内容的提问来源于stack exchange,提问作者Andrea Giannoni

