Singleton doesn't have unique instance Flutter - flutter

I'm trying to implement the singleton pattern with null safety in Flutter, to do a unique instance for my sqflite database, but after initialization in the splashScreen, when I try to access it in another Widget, it seems that it's not the same instance since the database is not initialized. I tried to add a random() int to check if it has the same value in the different widget, and the value change each times I request it.
My code is like this :
class DatabaseHandler {
final int random = new Random().nextInt(200);
static final DatabaseHandler instance = new DatabaseHandler._internal();
factory DatabaseHandler() {
return instance;
}
DatabaseHandler._internal();
late Database _database;
Database getDb() {
print(random);
return _database;
}
Future<void> initDB() async {
var path = await getDatabasesPath();
var dbPath = join(path, 'test.db');
Database dbConnection = await openDatabase(dbPath, version: 1,
onCreate: (Database db, int version) async {
return db.execute(
"CREATE TABLE favorite_page(id TEXT PRIMARY KEY, isFavorite BOOL)",
);
});
this._database = dbConnection;
}
}
I got a "Not initialize exception" when I do getDb() even if I used initDB() in the splashScreen.
I call the singleton in two places :
The db is initialized here :
class _SplashPageState extends State<SplashPage> {
static const String route = "/splash";
void initializeFlutterFire() async {
// Wait for Firebase to initialize and set `_initialized` state to true
await Firebase.initializeApp();
PushNotificationService pushNotif = PushNotificationService();
pushNotif.initialise();
}
#override
initState() {
super.initState();
WidgetsFlutterBinding.ensureInitialized();
initializeFlutterFire();
DatabaseHandler().initDB();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Lottie.asset('assets/lotties/loading-screen.json',
animate: true, repeat: true)));
}
}
and used here :
class PagesDb {
Future<FavoritePage> insert(FavoritePage page) async {
var db = DatabaseHandler().getDb();
await db.insert(tablePage, page.toMap());
return page;
}
}
But I tried to just use DatabaseHandler().getDb() in different widgets, they got the same random value, but not the same as in the splash screen

Related

Flutter provider listeners not updating themselves when sqflite database data changes

I created a local database using flutter sqflite. And I want to listen to a length of a list of tasks on that database and update the total count of the tasks, when I add something or remove from that list. But when I call provider.of(context) thing, it doesn't update themselves, means it doesn't listen. I used a stream to grab the database data and show in the UI.
Here is the database class I created:
class TaskDatabase with ChangeNotifier {
final String dbName = 'db.sqlite';
Database? _db;
List<Task> _tasksList = [];
int _totalTaskCount = 0;
final _streamController = StreamController<List<Task>>.broadcast();
Stream<List<Task>> all() =>
_streamController.stream.map((tasks) => tasks..sort());
int get totalTasksCount {
return _totalTaskCount;
}
Future<bool> close() async {
final db = _db;
if (db == null) {
return false;
}
await db.close();
return true;
}
Future<bool> open() async {
if (_db != null) {
return true;
}
final directory = await getApplicationDocumentsDirectory();
final path = '${directory.path}/$dbName';
try {
final db = await openDatabase(path);
_db = db;
//creating the database table using sqflite
const createTable = '''CREATE TABLE IF NOT EXISTS "TABLEOFTASKS" (
"id" INTEGER NOT NULL,
"taskTitle" TEXT,
"isDone" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("id" AUTOINCREMENT));''';
await db.execute(createTable);
// read all existing task objects from the db
_tasksList = await _fetchTasks();
_streamController.add(_tasksList);
return true;
} catch (e) {
// print('error = $e');
return false;
}
}
// Creating a new task and save to the database:
// other CRUD functions are not added here:)
Future<bool> create(String taskTitle) async {
final db = _db;
if (db == null) {
return false;
}
try {
final id = await db.insert(
'TABLEOFTASKS',
{
'taskTitle': taskTitle,
'isDone': 0,
},
);
final task = Task(
id: id,
taskTitle: taskTitle,
isDone: false,
);
_tasksList.add(task);
_streamController.add(_tasksList);
_totalTaskCount = _tasksList.length;
notifyListeners();
return true;
} catch (e) {
print('error in creating task = $e');
return false;
}
}
}
Here is the widget that I want to listen and update:
final int taskCount = Provider.of<TaskDatabase>(context, listen: true).totalTasksCount;
.
.
.
Text(taskCount.toString()),
I added the provider at the top of the widget tree and there are no errors. Only thing happening is not updating the text widget
I created a streamBuilder and grabbed the list I want as a snapshot. Updating the list length using the provider package did not work. You can find in the DB class in the question to find how I created a stream of Tasks. Firest initialize the Database in init method.
late final TaskDatabase _crudStorage;
#override
void initState() {
_crudStorage = TaskDatabase();
_crudStorage.open();
super.initState();
}
#override
void dispose() {
_crudStorage.close();
super.dispose();
}
....
return Scaffold(
resizeToAvoidBottomInset: false,
drawer: const CustomDrawer(),
body: StreamBuilder(
stream: _crudStorage.all(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.active:
case ConnectionState.waiting:
if (snapshot.data == null) {
return const Center(child: Shimmer());
}
final tasksList = snapshot.data as List<Task>; /// The List I want
.
.
.
.
.
.
SomeTextWidget('The length of tasks = ${tasksList.length}'),

Flutter : insert data to tables in sqflite at initialaztion database

I have created helper class for sqlite database and tried to insert data to tables in intialzing database as this :
class DbHelper {
static final DbHelper _instance = DbHelper.internal();
factory DbHelper() => _instance;
DbHelper.internal();
static Database _db;
//---------------- Create database method-------------------
Future<Database> createDatabase() async {
var teamsTable = SqliteData().teams;
var clubsTable = SqliteData().clubs;
if (_db != null) {
return _db;
}
String path = join(await getDatabasesPath(), 'clubsFantasy.db');
_db = await openDatabase(path, version: 1,
onCreate: (Database db, int v) async {
await db.execute('''
create table teams (club varchar(50) ,
price integer ,
surename varchar(3),
id integer,
league varchar(20))
''');
await db.execute(
'create table clubs (id integer ,club varchar(50) ,league_id integer,price integer ,surename varchar(3), league_name varchar(20),counter integer , selected varchar(5))');
teamsTable.map((e) => {db.insert('teams', e.toMap())}).toList();
clubsTable.map((e) => {db.insert('clubs', e.toMap())}).toList();
});
return _db;
}
//---------------- load data method---------------------
Future getTeamsData() async {
Database db = await createDatabase();
return db.query('teams');
}
Future getClubsData() async {
Database db = await createDatabase();
return db.query('clubs');
}
}
I call database at splash screen to create database and insert data to tables:
DbHelper helper;
#override
void initState() {
helper = DbHelper();
checkLogin();
helper.createDatabase();
super.initState();
}
now I want to display data and used it in provider ,I did this :
class TeamsProvider with ChangeNotifier {
DbHelper helper;
get clubs => helper.getClubsData();
get teams => helper.getTeamsData();
}
I called teams in future builder in screen :
Widget build(BuildContext context) {
return Consumer<TeamsProvider>(
builder: (context, teamsProv, child) {
return FutureBuilder(
future: teamsProv.teams,
builder: (context, snapshot) {
if (snapshot.hasData) {.....etc
I get error :
The method 'getTeamsData' was called on null. Receiver: null Tried calling: getTeamsData()
I am not sure but try declaring your DbHelper like this:
class DbHelper {
factory DbHelper() => DbHelper._internal()
DbHelper._internal();
DbHelper methods like:
static Future<Database> createDatabase() async {...
static Future getTeamsData() async {...
static Future getClubsData() async {...
And then:
class TeamsProvider with ChangeNotifier {
get clubs => DbHelper.getClubsData();
get teams => DbHelper.getTeamsData();
}

Flutter - Widget that no longer appears in the widget tree or this error might indicate a memory leak Warning

I have 3 page I check transitions with bottomNavigationBar first page is Soclose in this page im gettting information from the database and print it on the screen.
I'm getting information from the database smoothly but when i switch screens my console gives warning messages. An error appears in the console, but the application is working properly. When changing screens and returning to the old page(Soclose page), an error page appears and disappears within milliseconds.
I cant find similar questions and i tried to make suggestions in the warnings but either I couldn't do it or the solutions don't work.
Related soclose dart file:
class _Closesevents extends State<Soclose> {
List<Event> eventList;
int eventListLen;
#override
void initState() {
try{
final Future<Database> dbFuture = DbHelper.initializeDatabase();
dbFuture.then((database) {
Future<List<Event>> eventListFuture = DbHelper().getEventList();
eventListFuture.then((eventList) {
setState(() {
this.eventList = eventList;
this.eventListLen = eventList.length;
});
});
});}
catch (e,s)
{
print("[ERROR] $e");
print("[ERROR TREE]\n$s");
}
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
child: new ListView.builder(
itemCount: eventListLen,
itemBuilder: (BuildContext context, int index) =>
buildTripCard(context, index)),
);
}
Widget buildTripCard(BuildContext context, int index)
...
Databasehelper file
import ...
class DbHelper {
static DbHelper _databaseHelper; // Singleton DatabaseHelper
static Database _database;
static final String _tablename = EventConstants.TABLE_NAME;
static final String _columnId = EventConstants.COLUMN_ID;
static final String _columnTitle = EventConstants.COLUMN_TITLE;
static final String _columnDate = EventConstants.COLUMN_DATE;
static final String _columnStartTime = EventConstants.COLUMN_STARTTIME;
static final String _columnFinishTime = EventConstants.COLUMUN_FINISHTIME;
static final String _columnDesc = EventConstants.COLUMN_DESCRIPTION;
static final String _columnIsActive = EventConstants.COLUMN_ISACTIVE;
DbHelper._createInstance(); // Named constructor to create instance of DatabaseHelper
factory DbHelper() {
if (_databaseHelper == null) {
_databaseHelper = DbHelper._createInstance(); // This is executed only once, singleton object
}
return _databaseHelper;
}
Future<Database> get database async {
if (_database == null) {
_database = await initializeDatabase();
}
return _database;
}
static Future<Database> initializeDatabase() async {
Directory directory = await getApplicationDocumentsDirectory();
String path = directory.path + 'takvimapp.db';
// Open/create the database at a given path
var notesDatabase = await openDatabase(path, version: 1, onCreate: _createDb);
return notesDatabase;
}
static void _createDb(Database db, int newVersion) async {
await db.execute('CREATE TABLE $_tablename ( $_columnId INTEGER PRIMARY KEY NOT NULL,$_columnTitle TEXT ,$_columnDate TEXT,$_columnStartTime TEXT,$_columnFinishTime TEXT,$_columnDesc TEXT,$_columnIsActive INTEGER);');
}
// Get all events --map
Future<List<Map<String, dynamic>>> getEventMapList() async {
Database db = await this.database;
var result = await db.query(_tablename, orderBy: '$_columnTitle ASC');
return result;
}
// Insert Operation: Insert a Event object to database
Future<int> insertEvent(Event event) async {
Database db = await this.database;
var result = await db.insert(_tablename, event.toMap());
return result;
}
// Update Operation: Update a Event object and save it to database
Future<int> updateEvent(Event event) async {
var db = await this.database;
var result = await db.update(_tablename, event.toMap(), where: '$_columnId = ?', whereArgs: [event.id]);
return result;
}
// Delete Operation: Delete a Event object from database
Future<int> deleteEvent(int id) async {
var db = await this.database;
int result = await db.rawDelete('DELETE FROM $_tablename WHERE $_columnId = $id');
return result;
}
// Get number of Event objects in database
Future<int> getCount() async {
Database db = await this.database;
List<Map<String, dynamic>> x = await db.rawQuery('SELECT COUNT (*) from $_tablename');
int result = Sqflite.firstIntValue(x);
return result;
}
// Convert map to list
Future<List<Event>> getEventList() async {
var eventMapList = await getEventMapList(); // Get 'Map List' from database
int count = eventMapList.length; // Count the number of map entries in db table
List<Event> eventList = List<Event>();
// For loop to create a 'Event List' from a 'Event List'
for (int i = 0; i < count; i++) {
eventList.add(Event.fromMap(eventMapList[i]));
}
return eventList;
}
static Future closeDb() => _database.close();
}
The error warning is constantly written to the console in an infinite loop.
To get rid of the warning, I need to close the app and restart the emulator.
Warning message:
E/flutter (30455): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: setState() >called after dispose(): _CountDownItemState#2bbc3(lifecycle state: defunct, not mounted)
E/flutter (30455): This error happens if you call setState() on a State object for a widget that no >longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its >build). This error can occur when code calls setState() from a timer or an animation callback.
E/flutter (30455): The preferred solution is to cancel the timer or stop listening to the animation >in the dispose() callback.
Another solution is to check the "mounted" property of this object >before calling setState() to ensure the object is still in the tree.
E/flutter (30455): This error might indicate a memory leak if setState() is being called because >another object is retaining a reference to this State object after it has been removed from the >tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
Solution:
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _db.getEventList(),
builder: (context, snapshot) {
if (snapshot.data == null) {
return Container(
child: Text("Loading....."),
);
} else {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(snapshot.data[index].title),
);
});
}
}),
);
}
The issue is with your initState function override. It's good practice to also call the super of initState, super.initState, before all other logic. Your Futures may be completing too quickly, and calling setState before the state is even initialized. Simply move super.initState(); as the first statement in the override. Ex.
#override
void initState() {
super.initState();//Always call this first
try{
final Future<Database> dbFuture = DbHelper.initializeDatabase();
dbFuture.then((database) {
Future<List<Event>> eventListFuture = DbHelper().getEventList();
eventListFuture.then((eventList) {
setState(() {
this.eventList = eventList;
this.eventListLen = eventList.length;
});
});
});}
catch (e,s)
{
print("[ERROR] $e");
print("[ERROR TREE]\n$s");
}
}
Edit: However, this can still lead to errors as setState could still be called before the widget is mounted. This is why the FutureBuilder widget exists. Wrap the widget that needs this Future data in your build method, pass the Future to the future parameter of the FutureBuilder and access the data with the AsyncSnapshot that the builder provides. See more about FutureBuilder.

Execute if database exists flutter

So basically what i want to do is:
When the user turns on the app for the first time,
The SQLite database is created and the data is fetched from the internet.
Until this is done, SetupPage() widget is displayed in scaffolds body or else Home() is displayed.
Now the code i wrote works perfectly for the first time, but when i open it the second time,
The SetupPage() only shows, it never goes back to the Home(). What am i doing wrong here ?
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart';
import 'package:path/path.dart';
import 'pages/home.dart';
import 'pages/SetUpPage.dart';
import 'package:sqflite/sqflite.dart';
class App extends StatefulWidget {
createState() {
return AppState();
}
}
class AppState extends State<App> {
final bgColor = const Color(0xFF1abc9c);
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
bool status = false;
Database database;
#override
void initState() {
initializeData();
super.initState();
}
void initializeData() async
{
var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'demo.db');
status = false;
Database database = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute(
'CREATE TABLE news (id INTEGER PRIMARY KEY, topic TEXT, img TEXT, newstitle TEXT, news TEXT, newslink TEXT)');
fetchData();
}
);
database.close();
}
#override
Widget build(context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text("UnFound News"),
backgroundColor: bgColor,
),
body: status ? Home() : SetUpPage(),
);
}
/*Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);*/
void fetchData() async {
var result = await get("https://api.myjson.com/bins/a0bvu");
var arr = json.decode(result.body)['post'];
for(int i = 0; i < arr.length; i++)
{
//TODO: Implement addition to database.
}
setState(() {
status = true;
});
}
}
You probably want to put the fetchData in a onOpen parameter instead of the onCreate parameter like this:
Database database = await openDatabase(path, version: 1,
onCreate: (Database db, int version) async {
await db.execute(
'CREATE TABLE news (id INTEGER PRIMARY KEY, topic TEXT, img TEXT, newstitle TEXT, news TEXT, newslink TEXT)');
await fetchData();
}, onOpen: (Database db) async {
setState(() {
status = true;
});
});
static Database _db;
Future<Database> get db async {
if(_db != null)
return _db;
_db = await initDb();
return _db;
}
//Creating a database with name test.dn in your directory
initDb() async {
io.Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "test.db");
var theDb = await openDatabase(path, version: 1, onCreate: _onCreate);
return theDb;
}
checkDb(){
//do operation
var dbClient = await db;
}

Flutter: How to use SharedPreferences synchronously?

I am using Shared Preferences in my Flutter app and what I would like to do is store SharedPreferences as a field on startup and then use it synchronously in the app. However I'm not sure if I'm not missing anything.
What I want to achieve is instead of:
method1() async {
SharedPreferences sp = await SharedPreferences.getInstance();
return sp.getString('someKey');
}
to
SharedPreferences sp;
//I would probably pass SharedPreferences in constructor, but the idea is the same
someInitMethod() async {
sp = await SharedPreferences.getInstance();
}
method1() {
return sp.getString('someKey');
}
method2() {
return sp.getString('someKey2');
}
method3() {
return sp.getString('someKey3');
}
In that way I would achieve synchronous access to sharedPrefs. Is it bad solution?
EDIT:
What is worth mentioning is that getInstance method will only check for instance and if there is any than it returns it, so as I see it, is that async is only needed to initialize instance. And both set and get methods are sync anyway.
static Future<SharedPreferences> getInstance() async {
if (_instance == null) {
final Map<String, Object> fromSystem =
await _kChannel.invokeMethod('getAll');
assert(fromSystem != null);
// Strip the flutter. prefix from the returned preferences.
final Map<String, Object> preferencesMap = <String, Object>{};
for (String key in fromSystem.keys) {
assert(key.startsWith(_prefix));
preferencesMap[key.substring(_prefix.length)] = fromSystem[key];
}
_instance = new SharedPreferences._(preferencesMap);
}
return _instance;
}
I use the same approach as the original poster suggests i.e. I have a global variable (actually a static field in a class that I use for all such variables) which I initialise to the shared preferences something like this:
in globals.dart:
class App {
static SharedPreferences localStorage;
static Future init() async {
localStorage = await SharedPreferences.getInstance();
}
}
in main.dart:
void main() {
start();
}
Async.Future start() async {
await App.init();
localStorage.set('userName','Bob');
print('User name is: ${localStorage.get('userName)'}'); //prints 'Bob'
}
The above worked fine but I found that if I tried to use App.localStorage from another dart file e.g. settings.dart it would not work because App.localStorage was null but I could not understand how it had become null.
Turns out the problem was that the import statement in settings.dart was import 'package:<packagename>/src/globals.dart'; when it should have been import 'globals.dart;.
#iBob101 's answer is good, but still, you have to wait before you use the SharedPreferences for the first time.
The whole point is NOT to await for your SharedPreferences and be sure that it will always be NOT NULL.
Since you'll have to wait anyway let's do it in the main() method:
class App {
static SharedPreferences localStorage;
static Future init() async {
localStorage = await SharedPreferences.getInstance();
}
}
And the main method:
void main() async{
await SharedPref.initSharedPref();
runApp(MyApp());
}
the line await SharedPref.initSharedPref(); takes ~100ms to execute. This is the only drawback as far as I can see.
But you definitely know that in every place in the app your sharedPreferenes instance in NOT NULL and ready for accessing it:
String s = App.localStorage.getString(PREF_MY_STRING_VALUE);
I think it's worthwhile
The cleanest way is to retrieve SharedPreferences in main method and pass it to MyApp as a dependency:
void main() async {
// Takes ~50ms to get in iOS Simulator.
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
runApp(MyApp(sharedPreferences: sharedPreferences));
}
class MyApp extends StatefulWidget {
final SharedPreferences sharedPreferences;
const MyApp({Key key, this.sharedPreferences})
: assert(sharedPreferences != null),
super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
// You can access shared preferences via widget.sharedPreferences
return ...
}
I made a simple way to using this PrefUtil class:
import 'package:shared_preferences/shared_preferences.dart';
class PrefUtil {
static late final SharedPreferences preferences;
static bool _init = false;
static Future init() async {
if (_init) return;
preferences = await SharedPreferences.getInstance();
_init = true;
return preferences;
}
static setValue(String key, Object value) {
switch (value.runtimeType) {
case String:
preferences.setString(key, value as String);
break;
case bool:
preferences.setBool(key, value as bool);
break;
case int:
preferences.setInt(key, value as int);
break;
default:
}
}
static Object getValue(String key, Object defaultValue) {
switch (defaultValue.runtimeType) {
case String:
return preferences.getString(key) ?? "";
case bool:
return preferences.getBool(key) ?? false;
case int:
return preferences.getInt(key) ?? 0;
default:
return defaultValue;
}
}
}
In main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
PrefUtil.init();
.....
Save it like:
PrefUtil.setValue("isLogin", true);
Get the value like:
PrefUtil.getValue("isLogin", false) as bool
By this, it will initialize only once and get it where ever you need.
You can use FutureBuilder to render the loading screen while waiting for SharedPreferences to be intialized for the first time in a singleton-like class. After that, you can access it synchronously inside the children.
local_storage.dart
class LocalStorage {
static late final SharedPreferences instance;
static bool _init = false;
static Future init() async {
if (_init) return;
instance = await SharedPreferences.getInstance();
_init = true;
return instance;
}
}
app_page.dart
final Future _storageFuture = LocalStorage.init();
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _storageFuture,
builder: (context, snapshot) {
Widget child;
if (snapshot.connectionState == ConnectionState.done) {
child = MyPage();
} else if (snapshot.hasError) {
child = Text('Error: ${snapshot.error}');
} else {
child = Text('Loading...');
}
return Scaffold(
body: Center(child: child),
);
},
);
}
my_page.dart
return Text(LocalStorage.instance.getString(kUserToken) ?? 'Empty');
call shared prefs on startup of a stateful main app (we call ours a initState() override of a StatefulWidget after super.initState())
after shared prefs inits, set the value to a field on main (ex: String _someKey)
inject this field into any child component
You can the call setState() on _someKey at you leisure and it will persist to children injected with your field