16、数据存储与展示:将采集到的光照数据存入SQLite数据库,使用RecyclerView展示历史数据。

好,咱们继续往前走。前面几章我们把光线传感器调通了,数据也能实时看到了。但有个问题——你关掉App再打开,之前的数据全没了。这就像你写日记,写完就烧掉,那还记个啥?

所以这一章,我们得把数据存起来。我个人的习惯是,在项目初期就考虑好数据持久化方案,不然后面改起来很痛苦。咱们就用Android自带的SQLite,轻量级,不用搭服务器,够用。

16.1 为什么选SQLite?

你可能会问:现在有Room、有Realm,为什么还要学SQLite?

嗯,这个问题我当年也纠结过。说白了,SQLite是Android系统自带的,零依赖。Room底层也是封装了SQLite。你理解了SQLite,再用Room就一通百通。我在一个嵌入式项目里,连Room都没法用,只能手写SQLite,那时候就庆幸自己基础打得牢。

SQLite的几个特点:

  • 轻量级:整个数据库就是一个文件
  • 零配置:不用安装,不用配置
  • 可靠:Google官方推荐,经过大量验证

16.2 设计数据表

存光照数据,我们需要记录什么?

至少三个字段:

  • 唯一ID(自增主键)
  • 光照值(lux)
  • 采集时间(时间戳)

我建议再加一个备注字段,方便以后扩展。比如记录一下当时是室内还是室外。别问我为什么,我在项目里吃过亏——数据存了一大堆,回头想分析,发现不知道哪个数据对应什么场景。

字段名 类型 说明
id INTEGER 主键,自增
lux FLOAT 光照强度
timestamp LONG 采集时间(毫秒)
note TEXT 备注(可选)

16.3 创建数据库帮助类

Android里操作SQLite,标准做法是继承SQLiteOpenHelper。我刚开始学的时候,直接拿SQLiteDatabase的静态方法搞,结果各种坑。后来老老实实写Helper类,世界清净了。

public class LightDataDbHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "light_data.db";
    private static final int DATABASE_VERSION = 1;

    public static final String TABLE_NAME = "light_records";
    public static final String COLUMN_ID = "id";
    public static final String COLUMN_LUX = "lux";
    public static final String COLUMN_TIMESTAMP = "timestamp";
    public static final String COLUMN_NOTE = "note";

    private static final String CREATE_TABLE =
            "CREATE TABLE " + TABLE_NAME + " (" +
                    COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    COLUMN_LUX + " REAL NOT NULL, " +
                    COLUMN_TIMESTAMP + " INTEGER NOT NULL, " +
                    COLUMN_NOTE + " TEXT)";

    public LightDataDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 生产环境要做迁移,这里简单处理
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
        onCreate(db);
    }
}
小提示:版本升级时别直接DROP表,会丢数据。正式项目里要用ALTER TABLE或者建临时表迁移。我早期一个App就因为这样被用户骂惨了...

16.4 插入数据

采集到光照值后,我们把它存进去。这里我封装了一个方法:

public void insertLightData(float lux, String note) {
    SQLiteDatabase db = this.getWritableDatabase();
    ContentValues values = new ContentValues();
    values.put(COLUMN_LUX, lux);
    values.put(COLUMN_TIMESTAMP, System.currentTimeMillis());
    values.put(COLUMN_NOTE, note);
    db.insert(TABLE_NAME, null, values);
    db.close();
}

你想想看,每次采集都调这个方法,数据就一条条存进去了。简单吧?

16.5 查询数据

存进去是为了取出来。我们要展示历史数据,一般按时间倒序排列,最新的在最上面。

public List<LightRecord> getAllRecords() {
    List<LightRecord> records = new ArrayList<>();
    SQLiteDatabase db = this.getReadableDatabase();
    Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null,
            COLUMN_TIMESTAMP + " DESC");

    if (cursor.moveToFirst()) {
        do {
            LightRecord record = new LightRecord();
            record.setId(cursor.getLong(cursor.getColumnIndex(COLUMN_ID)));
            record.setLux(cursor.getFloat(cursor.getColumnIndex(COLUMN_LUX)));
            record.setTimestamp(cursor.getLong(cursor.getColumnIndex(COLUMN_TIMESTAMP)));
            record.setNote(cursor.getString(cursor.getColumnIndex(COLUMN_NOTE)));
            records.add(record);
        } while (cursor.moveToNext());
    }
    cursor.close();
    db.close();
    return records;
}
注意:Cursor用完后一定要关闭。我曾经因为忘记关闭Cursor,导致内存泄漏,App跑一会儿就OOM了。排查了好久才发现是这里的问题。

16.6 用RecyclerView展示数据

数据取出来了,怎么展示?用ListView?别,那东西太老了。RecyclerView才是现代Android的标配。它更灵活,性能更好。

首先,定义一个数据模型:

public class LightRecord {
    private long id;
    private float lux;
    private long timestamp;
    private String note;

    // getter和setter省略,自己补上
}

然后,写一个Adapter:

public class LightRecordAdapter extends RecyclerView.Adapter<LightRecordAdapter.ViewHolder> {

    private List<LightRecord> records;

    public LightRecordAdapter(List<LightRecord> records) {
        this.records = records;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_light_record, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        LightRecord record = records.get(position);
        holder.tvLux.setText(String.format("%.1f lux", record.getLux()));
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
        holder.tvTime.setText(sdf.format(new Date(record.getTimestamp())));
        holder.tvNote.setText(record.getNote() != null ? record.getNote() : "");
    }

    @Override
    public int getItemCount() {
        return records.size();
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        TextView tvLux, tvTime, tvNote;

        ViewHolder(View itemView) {
            super(itemView);
            tvLux = itemView.findViewById(R.id.tv_lux);
            tvTime = itemView.findViewById(R.id.tv_time);
            tvNote = itemView.findViewById(R.id.tv_note);
        }
    }
}

布局文件item_light_record.xml也很简单,三个TextView排成一行或者一列,看你喜好。

最后,在Activity或Fragment里把RecyclerView和Adapter连起来:

RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));

LightDataDbHelper dbHelper = new LightDataDbHelper(this);
List<LightRecord> records = dbHelper.getAllRecords();

LightRecordAdapter adapter = new LightRecordAdapter(records);
recyclerView.setAdapter(adapter);

搞定。运行一下,你就能看到历史数据一条条列出来了。

16.7 整体流程梳理

这一套下来,其实就三个步骤:存、取、展示。我画了个图,帮你理清思路:

光线传感器 采集 SQLite数据库 查询 RecyclerView 数据流向:传感器 → 存储 → 展示 核心:SQLite负责持久化,RecyclerView负责高效展示

16.8 避坑指南

最后,分享几个我踩过的坑:

  • 数据库线程问题:不要在UI线程做耗时数据库操作。虽然SQLite很快,但数据量大时还是会卡。我建议用AsyncTask或者Room的协程。
  • Cursor没关闭:前面说过了,血的教训。每次用完Cursor,记得close。
  • RecyclerView刷新:插入新数据后,记得调用adapter.notifyDataSetChanged()或者更精准的notifyItemInserted()。不然你插了数据,界面还是老样子。
  • 时间格式化:存时间戳用long,展示时再格式化。别存格式化后的字符串,不然排序、计算都很麻烦。

好了,这一章的内容就到这。你把代码跑起来,看看历史数据是不是乖乖躺在列表里了?嗯,那种感觉还是挺爽的。


公众号:蓝海资料掘金营,微信deep3321