健身App切换Fragment后数据丢失:新线程实现问题排查
问题分析与解决方案
问题描述
我开发了一款健身App,通过SQLiteDatabase读取数据,在Home Fragment中填充TextView与RecyclerView,原本运行完全正常。由于原启动速度较慢,我将TotalSetsWeek和storeDatainArrays方法迁移至新线程类以提升性能,现在首次打开App时功能正常,但切换离开该Fragment再返回后,所有数据都会被清空变为空白。
原代码
package com.example.polemistesfitness; import android.annotation.SuppressLint; import android.database.Cursor; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.os.Handler; import android.os.Looper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; public class HomeFragment extends Fragment { DatabaseHelper mydb; RecyclerView recyclerView; ArrayList<String> exercise_name, set_weight, set_reps, set_date; ArrayList<String> total_weight, total_reps; //for the weekly stats //making instance of db adapter class DBAdapter dbadapter; SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); String todaydate; String weekAgoDate; int totalMonthlyReps=0; int totalMonthlyWeight=0; int totalMonthSets=0; TextView weighttxt; TextView repstxt; TextView setstxt; TextView fitscoretxt; int fitnessScore; public HomeFragment() { } @SuppressLint("MissingInflatedId") @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_home, container, false); recyclerView = (RecyclerView) view.findViewById(R.id.homerecyclerView); //resetting in case a new set was added totalMonthlyReps=0; totalMonthlyWeight=0; totalMonthSets=0; fitnessScore=0; mydb = new DatabaseHelper(getContext()); exercise_name = new ArrayList<>(); set_weight = new ArrayList<>(); set_reps = new ArrayList<>(); set_date= new ArrayList<>(); total_weight= new ArrayList<>(); total_reps= new ArrayList<>(); todaydate = sdf.format(new Date()).toString(); ///weekAgoDate = sdf.format(new Date(System.currentTimeMillis() - 7L * 24 * 3600 * 1000)).toString(); //System.out.println("Week Ago Date: "+weekAgoDate); //This two methods need to be on different threads storeDatainArrays(); TotalSetsWeek(todaydate); dbadapter = new DBAdapter(getContext(),exercise_name ,set_weight,set_reps,set_date); recyclerView.setAdapter(dbadapter); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); //for loop to get total sets and weight for the week and put into textviews for(int i=0;i<total_weight.size();i++) { totalMonthlyWeight += Integer.parseInt(total_weight.get(i)); totalMonthlyReps += Integer.parseInt(total_reps.get(i)); System.out.println("Total sets: " + total_weight.size()); } //Fitness score is total number of sets by 2 in the month if(totalMonthlyWeight==0){ fitnessScore=0; } else{ fitnessScore = ((totalMonthlyReps+totalMonthlyWeight) /(total_weight.size())); fitnessScore = totalMonthlyReps+totalMonthlyWeight; } weighttxt = view.findViewById(R.id.totalweighttxt); weighttxt.setText(String.valueOf(totalMonthlyWeight)); repstxt = view.findViewById(R.id.totalrepstxt); repstxt.setText(String.valueOf(totalMonthlyReps)); setstxt = view.findViewById(R.id.totalsetstxt); setstxt.setText(String.valueOf(total_weight.size())); fitscoretxt = view.findViewById(R.id.fitscoretxt); fitscoretxt.setText(String.valueOf(fitnessScore)); return view; } //Storing the data of the past week void TotalSetsWeek(String todaydate) { TotalSetsWeekThread thread2 = new TotalSetsWeekThread(); thread2.start(); } //Storing Todays Data void storeDatainArrays(){ /*//calling the method in db java class and storing it in cursor Cursor cursor = mydb.readDate(todaydate); //Im going to change this to read only todays Data if(cursor.getCount() == 0){ Toast.makeText(getContext(), "Start a Workout Today", Toast.LENGTH_SHORT).show(); } //Here we read the columns by their number else{ while (cursor.moveToNext()){ //these columindexes might need changing exercise_name.add(cursor.getString(1)); //storing column data in arraylist set_weight.add(cursor.getString(2)); set_reps.add(cursor.getString(3)); set_date.add(cursor.getString(4)); } }*/ StoreDatainArrays thread1 = new StoreDatainArrays(); thread1.start(); } class StoreDatainArrays extends Thread { StoreDatainArrays(){ } @Override public void run(){ Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { //calling the method in db java class and storing it in cursor Cursor cursor = mydb.readDate(todaydate); //Im going to change this to read only todays Data if(cursor.getCount() == 0){ Toast.makeText(getContext(), "Start a Workout Today", Toast.LENGTH_SHORT).show(); } //Here we read the columns by their number else{ while (cursor.moveToNext()){ //these columindexes might need changing exercise_name.add(cursor.getString(1)); //storing column data in arraylist set_weight.add(cursor.getString(2)); set_reps.add(cursor.getString(3)); set_date.add(cursor.getString(4)); } } } }); } } class TotalSetsWeekThread extends Thread{ TotalSetsWeekThread() {} @Override public void run(){ Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { //calling the method in db java class and storing it in cursor Cursor cursor = mydb.readDate(todaydate); //Im going to change this to read only todays Data if(cursor.getCount() == 0){ Toast.makeText(getContext(), "Start a Workout Today", Toast.LENGTH_SHORT).show(); } //Here we read the columns by their number else{ while (cursor.moveToNext()){ //these columindexes might need changing System.out.println(cursor.getString(2)); total_weight.add(cursor.getString(2)); total_reps.add(cursor.getString(3)); } } } }); } } }
问题根源
- 异步线程完全失效:你创建的两个线程内部,通过
Handler.post把数据库查询操作又切回了主线程,等于数据库操作还是在主线程执行,既没解决启动慢的问题,还因为线程调度延迟导致数据加载时机混乱。 - 数据计算与线程执行不同步:
onCreateView中直接对空的ArrayList进行统计计算并设置TextView内容,首次打开时线程任务可能刚好在计算前完成,但切换返回时,线程任务还未执行,ArrayList仍为空,导致统计结果全为0。 - 未通知RecyclerView更新:线程往ArrayList添加数据后,没有调用
dbadapter.notifyDataSetChanged(),即使数据加载完成,RecyclerView也不会刷新。 - 生命周期适配错误:每次返回Fragment都会重新执行
onCreateView,创建新的集合和Adapter,但线程任务的执行与Fragment生命周期不同步,导致数据丢失或未及时加载。
解决方案
1. 真正实现异步数据库操作
将数据库查询放在子线程执行,仅更新UI的操作切回主线程:
class StoreDatainArrays extends Thread { @Override public void run() { // 子线程执行数据库查询 Cursor cursor = mydb.readDate(todaydate); ArrayList<String> tempExerciseNames = new ArrayList<>(); ArrayList<String> tempSetWeights = new ArrayList<>(); ArrayList<String> tempSetReps = new ArrayList<>(); ArrayList<String> tempSetDates = new ArrayList<>(); if(cursor.getCount() == 0){ // 主线程显示Toast new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(getContext(), "今天开始训练吧", Toast.LENGTH_SHORT).show() ); } else { while (cursor.moveToNext()){ tempExerciseNames.add(cursor.getString(1)); tempSetWeights.add(cursor.getString(2)); tempSetReps.add(cursor.getString(3)); tempSetDates.add(cursor.getString(4)); } cursor.close(); // 关闭Cursor避免内存泄漏 } // 主线程更新数据与UI new Handler(Looper.getMainLooper()).post(() -> { exercise_name.clear(); exercise_name.addAll(tempExerciseNames); set_weight.clear(); set_weight.addAll(tempSetWeights); set_reps.clear(); set_reps.addAll(tempSetReps); set_date.clear(); set_date.addAll(tempSetDates); if(dbadapter != null){ dbadapter.notifyDataSetChanged(); // 通知Adapter更新 } calculateAndUpdateStats(); // 重新计算统计数据 }); } }
2. 分离统计计算逻辑
将统计计算抽成单独方法,在线程加载完数据后调用:
private void calculateAndUpdateStats() { totalMonthlyReps = 0; totalMonthlyWeight = 0; fitnessScore = 0; for(int i = 0; i < total_weight.size(); i++) { totalMonthlyWeight += Integer.parseInt(total_weight.get(i)); totalMonthlyReps += Integer.parseInt(total_reps.get(i)); } fitnessScore = totalMonthlyWeight == 0 ? 0 : totalMonthlyReps + totalMonthlyWeight; weighttxt.setText(String.valueOf(totalMonthlyWeight)); repstxt.setText(String.valueOf(totalMonthlyReps)); setstxt.setText(String.valueOf(total_weight.size())); fitscoretxt.setText(String.valueOf(fitnessScore)); }
3. 修正TotalSetsWeekThread异步逻辑
class TotalSetsWeekThread extends Thread{ @Override public void run(){ Cursor cursor = mydb.readDate(todaydate); ArrayList<String> tempTotalWeight = new ArrayList<>(); ArrayList<String> tempTotalReps = new ArrayList<>(); if(cursor.getCount() == 0){ new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(getContext(), "今天开始训练吧", Toast.LENGTH_SHORT).show() ); } else{ while (cursor.moveToNext()){ tempTotalWeight.add(cursor.getString(2)); tempTotalReps.add(cursor.getString(3)); } cursor.close(); } new Handler(Looper.getMainLooper()).post(() -> { total_weight.clear(); total_weight.addAll(tempTotalWeight); total_reps.clear(); total_reps.addAll(tempTotalReps); calculateAndUpdateStats(); // 更新统计数据 }); } }
4. 优化Fragment初始化流程
@Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_home, container, false); // 初始化View引用 recyclerView = view.findViewById(R.id.homerecyclerView); weighttxt = view.findViewById(R.id.totalweighttxt); repstxt = view.findViewById(R.id.totalrepstxt); setstxt = view.findViewById(R.id.totalsetstxt); fitscoretxt = view.findViewById(R.id.fitscoretxt); // 初始化集合 exercise_name = new ArrayList<>(); set_weight = new ArrayList<>(); set_reps = new ArrayList<>(); set_date= new ArrayList<>(); total_weight= new ArrayList<>(); total_reps= new ArrayList<>(); mydb = new DatabaseHelper(getContext()); todaydate = sdf.format(new Date()).toString(); // 初始化RecyclerView dbadapter = new DBAdapter(getContext(),exercise_name ,set_weight,set_reps,set_date); recyclerView.setAdapter(dbadapter); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); // 启动异步线程加载数据 storeDatainArrays(); TotalSetsWeek(todaydate); return view; }
5. 资源释放
在Fragment销毁时关闭数据库连接,避免内存泄漏:
@Override public void onDestroy() { super.onDestroy(); if(mydb != null){ mydb.close(); // 假设DatabaseHelper实现了close方法 } }
额外建议
- 推荐使用Android官方的Coroutines替代Thread+Handler,代码更简洁且更易管理生命周期。
- 数据库操作务必关闭Cursor和数据库连接,避免内存泄漏。
内容的提问来源于stack exchange,提问作者user21233522
相关产品推荐
相关产品推荐

