objectbox / objectbox-dart

Flutter database for super-fast Dart object persistence
https://docs.objectbox.io/getting-started
Apache License 2.0
927 stars 115 forks source link

Flutter ObjectBox does not save list item after re-opening app #518

Closed DktPhl2019 closed 1 year ago

DktPhl2019 commented 1 year ago

I have a textfield input.

Clicking Add should add the input text from the textfield to a SwitchListTile list.

Clicking Remove should remove the last item from the list.

Issue: After I add an item, close the app, and re-open the app, the item added is not saved.

I am using ObjectBox nonsql database Android Studio on Windows 11(vm: Pixel API Tiramisu)

pubspec.yaml

name: flutter_dk_phila_1
description: A new Flutter project.
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=2.19.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  objectbox: ^1.7.2
  objectbox_flutter_libs: any
  path:
  path_provider:
  intl: ^0.18.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0
  build_runner: ^2.3.3
  objectbox_generator: any

flutter:
  uses-material-design: true

main.dart

import 'package:flutter/material.dart';
import 'model.dart';
import 'objectbox.dart';

late ObjectBox objectbox;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  objectbox = await ObjectBox.create();
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  static TextEditingController mc = TextEditingController();
  Task? existingTask;
  static final List<Task> tasks=<Task>[];
  bool isSelected=false;

  addItemToList() {
    if (mc.text.isNotEmpty) {
      setState(() {
        objectbox.saveTask(existingTask, mc.text);
        tasks.add(Task(text: mc.text, id: tasks.length + 1, isSelected: false));
      });
    }
  }

  deleteItemFromList() {
    if(tasks[index].id!=0 && tasks.isNotEmpty && tasks[index].isSelected!= true) {
      setState(() {
        objectbox.removeTask(tasks[index].id);
        tasks.removeLast();
      });
    }
  }

  getTaskId(){
    if(tasks[index].id!=null && tasks[index].id!=0) {
      objectbox.getTask(tasks[index].id);
    }
  }

  int index = 0;

  @override
  Widget build(BuildContext context) {
    Widget space = SizedBox(width: 100, height: 5, child: Text(""));
    Widget myinput = TextField(controller: mc,decoration:InputDecoration(border: OutlineInputBorder(), labelText: 'Name'));
    Widget btnAdd = ElevatedButton(onPressed: addItemToList, child: Text('Add'));
    Widget btnRemove = ElevatedButton(onPressed: deleteItemFromList, child: Text('Remove'));
    Widget mylist = SizedBox(width: 800,height: 500,child: ListView.builder(
      shrinkWrap: true,
      itemCount: tasks.length,
      itemBuilder: (BuildContext context, int index) {
        return SingleChildScrollView(
          child: Column(mainAxisAlignment: MainAxisAlignment.spaceEvenly,mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              SwitchListTile(
                  title: Text(tasks[index].text+" id: "+ getTaskId(), style: TextStyle(fontSize: 11)),
                  value: tasks[index].isSelected,
                  onChanged: (bool value) {
                    {
                      setState(() {
                        mc.text=tasks[index].text;
                      });
                    };
                  })
            ],
          ),
        );
      },
    ),
    );

    return MaterialApp(
      home: Scaffold(
        resizeToAvoidBottomInset: false,
        body: SingleChildScrollView(
          scrollDirection: Axis.vertical,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Container(
                padding: const EdgeInsets.all(5),
                child: SingleChildScrollView(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: <Widget>[
                      space,
                      myinput,
                      space,
                      btnAdd,
                      space,
                      btnRemove,
                      space,
                      mylist,
                      space
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

model.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:objectbox/objectbox.dart';

@Entity()
class Task {
  @Id()
  int id;
  String text;
  bool isSelected;

  @Property(type: PropertyType.date)
  DateTime dateCreated;

  @Property(type: PropertyType.date)
  DateTime? dateFinished;

  Task({this.id = 0, required this.text, this.isSelected = false,DateTime? dateCreated}): dateCreated = dateCreated ?? DateTime.now();

  String get dateCreatedFormat => DateFormat('MM.dd.yy HH:mm:ss').format(dateCreated);
  String get dateFinishedFormat => DateFormat('MM.dd.yy HH:mm:ss').format(dateFinished!);

  bool isFinished() {return dateFinished != null;}

  void toggleFinished() {
    if (isFinished()) {dateFinished = null;} else {dateFinished = DateTime.now();}
  }

  String getStateText() {
    String text;
    if (isFinished()) {text = 'Finished on $dateFinishedFormat';} else {text = 'Created on $dateCreatedFormat';}
    return text;
  }
}

objectbox.dart

import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'model.dart';
import 'objectbox.g.dart';

class ObjectBox {
  late final Store store;
  late final Admin _admin;
  late final Box<Task> taskBox;

  ObjectBox._create(this.store) {
    if (Admin.isAvailable()) {
      _admin = Admin(store);
    }
    taskBox = Box<Task>(store);
  }

  static Future<ObjectBox> create() async {
    final documentsDirectory = await getApplicationDocumentsDirectory();
    final databaseDirectory = p.join(documentsDirectory.path, "obx-demo-relations");
    final store = await openStore(directory: databaseDirectory);
    return ObjectBox._create(store);
  }

  Stream<List<Task>> getTasks() {
    final qBuilderTasks = taskBox.query().order(Task_.dateCreated, flags: Order.descending);
    return qBuilderTasks
        .watch(triggerImmediately: true)
        .map((query) => query.find());
  }

  void saveTask(Task? task, String text) {
    if (text.isEmpty) {
      return;
    }
    if (task == null) {
      task = Task(text: '');
    } else {
      task.text = text;
    }
    taskBox.put(task);
  }

  void getTask(int taskId){
    if(taskId!=0){
      taskBox.get(taskId);
    }
  }

  void removeTask(int taskId) {
    if(taskId!=0){
      taskBox.remove(taskId);
    }
  }

}
greenrobot-team commented 1 year ago

This condition doesn't look right to me, maybe it should be isEmpty instead?

  void saveTask(Task? task, String text) {
    if (text.isNotEmpty) {
      return;
    }
DktPhl2019 commented 1 year ago

Hello! I updated the code from text.isNotEmpty to text.isEmpty, and I still see that nothing is saved. Could you please take a look. Thank You

greenrobot-team commented 1 year ago

@DktPhl2019 Based on a quick look, I don't see anything else wrong with your code. I suggest to debug it and find out if taskBox.put(task) is called at all.

Edit: you can also configure ObjectBox Admin to help you see what's stored in the database: https://docs.objectbox.io/data-browser.

DktPhl2019 commented 1 year ago

Hello! Please review the code above. The code is returning zero or null ID which means that the code taskBox.put(task) is not creating a non-zero ID. This is the bug. Could you please fix the issue. Thank You

I added the following code:

  1. ObjectBox:
    void getTask(int taskId){
    if(taskId!=0){
      taskBox.get(taskId);
    }
    }
  2. main a.
    getTaskId(){
    if(tasks[index].id!=null && tasks[index].id!=0) {
      objectbox.getTask(tasks[index].id);
    }
    }

    b.

    SwitchListTile(
                  title: Text(tasks[index].text+" id: "+ getTaskId(), style: TextStyle(fontSize: 11)),
greenrobot-team commented 1 year ago

The code is returning zero or null ID which means that the code taskBox.put(task) is not creating a non-zero ID. This is the bug.

@DktPhl2019 Then please provide a unit test that reproduces this. Then we can fix the issue. I don't have the time to debug your application.

DktPhl2019 commented 1 year ago

Hello! I am not able to run a unit test on my case b/c I don't know how to setup it properly. I can't reference the variables from the lib folder under the test folder and I am not sure how I will go from my specific case variables to the general case of taskBox.input method returning a null ID. I am kind of newbie with Flutter unit testing. I guess you could close the issue. Instead, I looked at this page: github.com/objectbox/objectbox-dart/tree/main/objectbox/example/flutter/objectbox_demo The problem here is that objectbox.dart is missing Note_class and openStore() method. Could you please add the missing pieces of the code. I want to run this demo code and do test an ID. Thank You

greenrobot-team commented 1 year ago

@DktPhl2019 The underscore class and openStore() method are part of code that is generated by ObjectBox. See the generate step on the Getting Started page for more info.

DktPhl2019 commented 1 year ago

Hello! I am not able to generate objectbox.g.dart file. I type: flutter pub run build_runner build Issue: I get error: [SEVERE] Nothing can be built, yet a build was requested. All done on Windows 11 using Android Studio.

Could you please check what I am missing. Thank You

My Files:

pubspec.yaml:
name: flutter_greenrobot_team_objectbox_example
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
  sdk: '>=2.19.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  intl: ^0.18.0
  build_runner: ^2.3.3
  objectbox: ^1.7.2
  objectbox_flutter_libs: any
  path: ^1.8.2
  path_provider: ^2.0.13

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true
main.dart```

import 'dart:async';

import 'package:flutter/material.dart';

import 'model.dart';
import 'objectbox.dart';

// ignore_for_file: public_member_api_docs

/// Provides access to the ObjectBox Store throughout the app.
late ObjectBox objectbox;

Future<void> main() async {
  // This is required so ObjectBox can get the application directory
  // to store the database in.
  WidgetsFlutterBinding.ensureInitialized();

  objectbox = await ObjectBox.create();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'OB Example',
    theme: ThemeData(primarySwatch: Colors.blue),
    home: const MyHomePage(title: 'OB Example'),
  );
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _noteInputController = TextEditingController();

  Future<void> _addNote() async {
    if (_noteInputController.text.isEmpty) return;
    await objectbox.addNote(_noteInputController.text);
    _noteInputController.text = '';
  }

  @override
  void dispose() {
    _noteInputController.dispose();
    super.dispose();
  }

  GestureDetector Function(BuildContext, int) _itemBuilder(List<Note> notes) =>
          (BuildContext context, int index) => GestureDetector(
        onTap: () => objectbox.noteBox.remove(notes[index].id),
        child: Row(
          children: <Widget>[
            Expanded(
              child: Container(
                decoration: const BoxDecoration(
                    border:
                    Border(bottom: BorderSide(color: Colors.black12))),
                child: Padding(
                  padding: const EdgeInsets.symmetric(
                      vertical: 18.0, horizontal: 10.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        notes[index].text,
                        style: const TextStyle(
                          fontSize: 15.0,
                        ),
                        // Provide a Key for the integration test
                        key: Key('list_item_$index'),
                      ),
                      Padding(
                        padding: const EdgeInsets.only(top: 5.0),
                        child: Text(
                          'Added on ${notes[index].dateFormat}',
                          style: const TextStyle(
                            fontSize: 12.0,
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
      );

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Column(children: <Widget>[
      Padding(
        padding: const EdgeInsets.all(20.0),
        child: Row(
          children: <Widget>[
            Expanded(
              child: Column(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 10.0),
                    child: TextField(
                      decoration: const InputDecoration(
                          hintText: 'Enter a new note'),
                      controller: _noteInputController,
                      onSubmitted: (value) => _addNote(),
                      // Provide a Key for the integration test
                      key: const Key('input'),
                    ),
                  ),
                  const Padding(
                    padding: EdgeInsets.only(top: 10.0, right: 10.0),
                    child: Align(
                      alignment: Alignment.centerRight,
                      child: Text(
                        'Tap a note to remove it',
                        style: TextStyle(
                          fontSize: 11.0,
                          color: Colors.grey,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            )
          ],
        ),
      ),
      Expanded(
          child: StreamBuilder<List<Note>>(
              stream: objectbox.getNotes(),
              builder: (context, snapshot) => ListView.builder(
                  shrinkWrap: true,
                  padding: const EdgeInsets.symmetric(horizontal: 20.0),
                  itemCount: snapshot.hasData ? snapshot.data!.length : 0,
                  itemBuilder: _itemBuilder(snapshot.data ?? []))))
    ]),
    // We need a separate submit button because flutter_driver integration
    // test doesn't support submitting a TextField using "enter" key.
    // See https://github.com/flutter/flutter/issues/9383
    floatingActionButton: FloatingActionButton(
      key: const Key('submit'),
      onPressed: _addNote,
      child: const Icon(Icons.add),
    ),
  );
}
``model.dart:

import 'package:intl/intl.dart'; import 'package:objectbox/objectbox.dart';

// ignore_for_file: public_member_api_docs

@Entity() class Note { int id;

String text; String? comment;

/// Note: Stored in milliseconds without time zone info. DateTime date;

Note(this.text, {this.id = 0, this.comment, DateTime? date}) : date = date ?? DateTime.now();

String get dateFormat => DateFormat('dd.MM.yyyy hh:mm:ss').format(date); }


objectbox.dart:

import 'package:objectbox/objectbox.dart';

import 'model.dart'; // created by flutter pub run build_runner build

/// Provides access to the ObjectBox Store throughout the app. /// /// Create this in the apps main function. class ObjectBox { /// The Store of this app. late final Store store;

/// A Box of notes. late final Box noteBox;

ObjectBox._create(this.store) { noteBox = Box(store);

// Add some demo data if the box is empty.
if (noteBox.isEmpty()) {
  _putDemoData();
}

}

/// Create an instance of ObjectBox to use throughout the app. static Future create() async { // Future openStore() {...} is defined in the generated objectbox.g.dart final store = await openStore(); return ObjectBox._create(store); }

void _putDemoData() { final demoNotes = [ Note('Quickly add a note by writing text and pressing Enter'), Note('Delete notes by tapping on one'), Note('Write a demo app for ObjectBox') ]; store.runInTransactionAsync(TxMode.write, _putNotesInTx, demoNotes); }

Stream<List> getNotes() { // Query for all notes, sorted by their date. // https://docs.objectbox.io/queries final builder = noteBox.query().order(Note_.date, flags: Order.descending); // Build and watch the query, // set triggerImmediately to emit the query immediately on listen. return builder .watch(triggerImmediately: true) // Map it to a list of notes to be used by a StreamBuilder. .map((query) => query.find()); }

static void _putNotesInTx(Store store, List notes) => store.box().putMany(notes);

/// Add a note within a transaction. /// /// To avoid frame drops, run ObjectBox operations that take longer than a /// few milliseconds, e.g. putting many objects, in an isolate with its /// own Store instance. /// For this example only a single object is put which would also be fine if /// done here directly. Future addNote(String text) => store.runInTransactionAsync(TxMode.write, _addNoteInTx, text);

/// Note: due to dart-lang/sdk#36983 /// not using a closure as it may capture more objects than expected. /// These might not be send-able to an isolate. See Store.runAsync for details. static void _addNoteInTx(Store store, String text) { // Perform ObjectBox operations that take longer than a few milliseconds // here. To keep it simple, this example just puts a single object. store.box().put(Note(text)); } }

objectbox-model.json:

{ "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.", "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.", "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.", "entities": [ { "id": "1:2802681814019499133", "lastPropertyId": "4:6451339597165131221", "name": "Note", "properties": [ { "id": "1:3178873177797362769", "name": "id", "type": 6, "flags": 1 }, { "id": "2:4285343053028527696", "name": "text", "type": 9 }, { "id": "3:2606273611209948020", "name": "comment", "type": 9 }, { "id": "4:6451339597165131221", "name": "date", "type": 10 } ], "relations": [] } ], "lastEntityId": "1:2802681814019499133", "lastIndexId": "0:0", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, "retiredEntityUids": [], "retiredIndexUids": [], "retiredPropertyUids": [], "retiredRelationUids": [], "version": 1 }

greenrobot-team commented 1 year ago

@DktPhl2019 It looks like you did miss running the second flutter pub add command mentioned at https://docs.objectbox.io/getting-started which adds the generator dependency.

DktPhl2019 commented 1 year ago

The app now works. Thank You