Flutter使用sqflite时出现database_closed异常的问题求助
解决sqflite操作时的
DatabaseException(error database_closed)问题 从你的错误日志和代码来看,核心问题是在执行数据库查询/插入操作时,数据库连接已经被提前关闭。下面分析具体原因并给出针对性的解决方案:
问题根源分析
你当前的实现是每次数据库操作都新建MovieProvider实例、打开连接、执行操作、关闭连接。这种模式看似合理,但存在两个潜在风险:
- 频繁的打开/关闭数据库连接可能触发sqflite内部的状态异常,尤其是在异步操作未完全完成时就执行关闭;
- 如果存在并发操作(比如快速触发两次查询),多个独立的数据库实例可能互相干扰,导致某个连接被意外标记为已关闭。
另外,你的paginate方法还存在一个小问题:使用maps.map()转换Movie对象时,因为Iterable的map是懒加载的,没有调用.toList()的话,实际不会执行转换逻辑(不过这不是导致数据库关闭错误的直接原因)。
解决方案:改用单例模式管理数据库连接
我们可以将MovieProvider改为单例,确保整个App生命周期内只有一个数据库实例,避免频繁打开/关闭连接,同时优化批量插入的效率。
1. 修改MoviesBloc为单例并优化连接管理
import 'dart:async'; import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; class MoviesBloc implements BaseBloc { // 单例实现 static final MoviesBloc _instance = MoviesBloc._internal(); factory MoviesBloc() => _instance; MoviesBloc._internal(); final _moviesController = StreamController<List<Movie>>(); Database? _moviesDB; // 改为可空类型,避免空指针 String? _moviesPath; Stream<List<Movie>> get moviesStream => _moviesController.stream; List<Movie> _movies = []; List<Movie> get movies => _movies; int getLengthMovieList() => _movies.length; void clearMovieList() => _movies.clear(); // 确保数据库只被打开一次 Future<void> openMovieDB({String dbName = 'movies.db'}) async { if (_moviesDB != null && _moviesDB!.isOpen) return; var databasesPath = await getDatabasesPath(); _moviesPath = join(databasesPath, dbName); _moviesDB = await openDatabase( _moviesPath!, version: 1, onCreate: (Database db, int version) async { await db.execute( 'CREATE TABLE movies (Id INTEGER PRIMARY KEY AUTOINCREMENT, Title TEXT NOT NULL, imdbRating TEXT, Poster TEXT, Plot TEXT, Saved INTEGER DEFAULT 0)' ); }, ); } // 安全关闭数据库 Future<void> closeMovieDB() async { if (_moviesDB != null && _moviesDB!.isOpen) { await _moviesDB!.close(); _moviesDB = null; } } @override void dispose() { _moviesController.close(); closeMovieDB(); // 页面销毁时统一关闭数据库 } Database get moviesDB { if (_moviesDB == null || !_moviesDB!.isOpen) { throw StateError('数据库未打开,请先调用openMovieDB'); } return _moviesDB!; } }
2. 修改MovieProvider为单例并优化操作
class MovieProvider extends MoviesBloc { static final MovieProvider _instance = MovieProvider._internal(); factory MovieProvider() => _instance; MovieProvider._internal(); final String _tableName = 'movies'; Future<Movie> insert(Movie movie, {ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.ignore}) async { await moviesDB.insert(_tableName, movie.toMap(), conflictAlgorithm: conflictAlgorithm); return movie; } // 改用事务批量插入,比并行插入更高效稳定 Future<bool> insertAll(List<Movie> movies) async { await moviesDB.transaction((txn) async { for (final movie in movies) { await txn.insert(_tableName, movie.toMap(), conflictAlgorithm: ConflictAlgorithm.ignore); } }); return true; } Future<List<Movie>> paginate(int page, {int limit = 15}) async { print('in Paginate to SQLite'); final maps = await moviesDB.query( _tableName, columns: ['Id', 'Title', 'imdbRating', 'Poster', 'Plot', 'Saved'], limit: limit, offset: page == 1 ? 0 : ((page - 1) * limit), ); // 直接转换为List,解决懒加载问题 return maps.map((movieMap) => Movie.fromJson(movieMap)).toList(); } }
3. 调整数据保存/加载方法
不再每次操作都关闭数据库,而是在App退出时统一关闭:
// 保存数据 static Future<bool> saveAllMoviesIntoSqlite(List<Movie> movies) async { final db = MovieProvider(); await db.openMovieDB(); await db.insertAll(movies); return true; } // 加载数据 static Future<Map> getAllMoviesFromSqlite(int page) async { final db = MovieProvider(); await db.openMovieDB(); final movies = await db.paginate(page); return {"currentPage": page, "movies": movies}; }
4. 在App根Widget中统一销毁数据库
比如在MaterialApp的State类中:
@override void dispose() { MovieProvider().dispose(); super.dispose(); }
额外优化点
- 使用事务批量插入代替
Future.wait并行插入,减少数据库IO开销,提升稳定性; - 增加数据库实例的非空校验,避免空指针异常;
- 修复
paginate方法中Movie对象转换的懒加载问题,确保数据正确转换。
内容的提问来源于stack exchange,提问作者Mohammad Shamsi
相关产品推荐
相关产品推荐

