Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
DktPhl2019 opened this issue Mar 12, 2023 · 10 comments
Closed

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

DktPhl2019 opened this issue Mar 12, 2023 · 10 comments
Labels
more info required Needs more info to become actionable. Auto-closed if no response. question How to do something/general question

Comments

@DktPhl2019
Copy link

DktPhl2019 commented Mar 12, 2023

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);
    }
  }

}


@DktPhl2019 DktPhl2019 added the bug Something isn't working label Mar 12, 2023
@greenrobot-team
Copy link
Member

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

  void saveTask(Task? task, String text) {
    if (text.isNotEmpty) {
      return;
    }

@greenrobot-team greenrobot-team added more info required Needs more info to become actionable. Auto-closed if no response. and removed bug Something isn't working labels Mar 13, 2023
@DktPhl2019
Copy link
Author

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

@github-actions github-actions bot removed the more info required Needs more info to become actionable. Auto-closed if no response. label Mar 14, 2023
@greenrobot-team
Copy link
Member

greenrobot-team commented Mar 14, 2023

@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.

@greenrobot-team greenrobot-team added question How to do something/general question more info required Needs more info to become actionable. Auto-closed if no response. labels Mar 14, 2023
@DktPhl2019
Copy link
Author

DktPhl2019 commented Mar 14, 2023

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);
    }
  }
  1. 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
Copy link
Member

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.

@greenrobot-team greenrobot-team added more info required Needs more info to become actionable. Auto-closed if no response. and removed more info required Needs more info to become actionable. Auto-closed if no response. labels Mar 14, 2023
@DktPhl2019
Copy link
Author

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

@github-actions github-actions bot removed the more info required Needs more info to become actionable. Auto-closed if no response. label Mar 19, 2023
@greenrobot-team
Copy link
Member

@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.

@greenrobot-team greenrobot-team added the more info required Needs more info to become actionable. Auto-closed if no response. label Mar 20, 2023
@DktPhl2019
Copy link
Author

DktPhl2019 commented Mar 20, 2023

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<Note> noteBox;

  ObjectBox._create(this.store) {
    noteBox = Box<Note>(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<ObjectBox> create() async {
    // Future<Store> 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<Note>> 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<Note> notes) =>
      store.box<Note>().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<void> addNote(String text) =>
      store.runInTransactionAsync(TxMode.write, _addNoteInTx, text);

  /// Note: due to [dart-lang/sdk#36983](https://github.com/dart-lang/sdk/issues/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<Note>().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
}
```
```

@github-actions github-actions bot removed the more info required Needs more info to become actionable. Auto-closed if no response. label Mar 21, 2023
@greenrobot-team
Copy link
Member

@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.

@greenrobot-team greenrobot-team added the more info required Needs more info to become actionable. Auto-closed if no response. label Mar 21, 2023
@DktPhl2019
Copy link
Author

The app now works. Thank You

@github-actions github-actions bot removed the more info required Needs more info to become actionable. Auto-closed if no response. label Mar 22, 2023
@greenrobot-team greenrobot-team added the more info required Needs more info to become actionable. Auto-closed if no response. label Mar 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
more info required Needs more info to become actionable. Auto-closed if no response. question How to do something/general question
Projects
None yet
Development

No branches or pull requests

2 participants