GetDutchie / brick

An intuitive way to work with persistent data in Dart
https://getdutchie.github.io/brick/#/
361 stars 28 forks source link

Problem Re-initializing #396

Closed SubutaDan closed 3 months ago

SubutaDan commented 3 months ago

My application shows a list of offline-first-with-REST items in a stateful widget. This has been stable for a long time.

The items belong to the user so, when a new user logs in to the application, I need to flush the list so the new user doesn't see the old user's data. I have recently started working on adding this functionality. So far it is not working.

My strategy is to have the user login handler, which is in a streambuilder, determine whether the user who has just logged in is the same user who had logged in previously. If so then it navigates to the list screen. by returning the list screen's widget. That is working as expected; the new user's items are displayed.

If the user who has just logged in is not the same user who had logged in previously, the login handler resets the repository, then navigates to the list screen by returning the list screen's widget . This is where the problem occurs.

When the list screen loads after a new user has been detected, it hangs on "Migrating database . . " indefinitely. with this error:

E/flutter ( 3632): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: DatabaseException(error database_closed)
E/flutter ( 3632): #0      SqfliteDatabaseMixin.checkNotClosed (package:sqflite_common/src/database_mixin.dart:457:7)
E/flutter ( 3632): #1      SqfliteDatabaseExecutorMixin.execute (package:sqflite_common/src/database_mixin.dart:43:8)
E/flutter ( 3632): #2      SqliteProvider.lastMigrationVersion (package:brick_sqlite/src/sqlite_provider.dart:148:14)
E/flutter ( 3632): <asynchronous suspension>
E/flutter ( 3632): #3      OfflineFirstRepository.migrate (package:brick_offline_first/src/offline_first_repository.dart:296:25)
E/flutter ( 3632): <asynchronous suspension>
E/flutter ( 3632): #4      OfflineFirstWithRestRepository.migrate (package:brick_offline_first_with_rest/src/offline_first_with_rest_repository.dart:145:5)
E/flutter ( 3632): <asynchronous suspension>
E/flutter ( 3632): #5      OfflineFirstRepository.initialize (package:brick_offline_first/src/offline_first_repository.dart:291:5)
E/flutter ( 3632): <asynchronous suspension>
E/flutter ( 3632): #6      OfflineFirstWithRestRepository.initialize (package:brick_offline_first_with_rest/src/offline_first_with_rest_repository.dart:136:5)
E/flutter ( 3632): <asynchronous suspension>
E/flutter ( 3632): #7      _KokosListScreenState.initState.<anonymous closure> (package:kokodoko_flutter_client/screens/kokos_list_screen.dart:68:40)
E/flutter ( 3632): <asynchronous suspension>
E/flutter ( 3632): 

If I then reload the list screen, either by logging out and then back in as the same user or by navigating to a different screen and pushing a button that causes the list screen to be reloaded, the list populates as expected.

Here is how the login handler resets the repository:

KokoRepository().reset().then(((_) {}));

And here is the line in the list screen that should allow the list screen to be populated:

KokoRepository().initialize().then((_) => setState(() => migrated = true));

Any thoughts on how I can make this work?

Thanks very much.

hortigado commented 3 months ago

Hi i use it code but also have a problem when the cookie expired the pending transactions not can upload to the server. I create a function for change the header cookie of each transaction.

When the user login you need initialize the repository for avoid the error database closed also every the app load first time

      Repository.configure(AppWrite.endpoint, token);
        await Repository().initialize();

SignOut

await Repository().sqliteProvider.resetDb();

SubutaDan commented 3 months ago

Thank you for the suggestion, @hortigado . I am exploring your suggestion but so far it has not resolved the issue.

hortigado commented 3 months ago

Hi, if you could provide more code, the problem could be seen more easily.

SubutaDan commented 3 months ago

Hi Hortigado,

Thanks very much for offering to look at my code.

Here is the code that determines that the login belongs to a different user than the one who had logged in previously (if any) and, if so, records the new user's email address in SharedPreferences, and reinitializes the repository:

                if (kokoerEmailFoundInPrefs == null ||
                    kokoerEmailFoundInPrefs != user?.email) {
                  logger.d("Treating this like a different user");

                  if (user?.email != null) {
                    String userEmailToSetInPrefs = user?.email as String;
                    logger.d("userEmailToSetInPrefs: $userEmailToSetInPrefs");

                    (snapshot.data?[0] as SharedPreferences)
                        .setString(
                            KokodokoConstants.of(context)!.kokoerEmailKey,
                            userEmailToSetInPrefs)
                        .then(((_) {}) as FutureOr Function(void value));
                  }

                  KokoRepository().reset().then((_) {});
               }

This is all inside a synchronous method, so the execution continues while the async methods (setString() and reset()) are running. I don't think this is the cause of the issue, because:

  1. There are no further dependencies on the email address in SharedPreferences in the workflow that is hanging.

  2. I tried adding a very long synchronous sleep after the reset(). The behavior did not change.

Here is the initState() method in the stateful widget that shows the list after migration:

  @override
  void initState() {
    logger.d("in initState(); migrated: $migrated");
    super.initState();
    autoScreenController = AutoScrollController(
        viewportBoundaryGetter: () => Rect.fromLTRB(
            0, 0, 0, MediaQuery.of(widget.context).padding.bottom),
        axis: scrollDirection);
    logger.d("about to initialize KokoRepository");
    //   KokoRepository().initialize().then((_) => setState(() => migrated = true));
    logger.d("about to migrate; migrated: $migrated");
    KokoRepository().initialize().then((_) {
      setState(() => migrated = true);
      logger.d("migrated: $migrated");

    });
  }

Here is the logged output from initState() the first time it runs for the new (different) user and hangs:

I/flutter ( 8314): │ 🐛 in initState(); migrated: false
I/flutter ( 8314): │ 🐛 about to migrate; migrated: false

Here is the output after I navigate away and then back to the stateful widget screen:

I/flutter ( 8314): │ 🐛 in initState(); migrated: false
I/flutter ( 8314): │ 🐛 about to migrate; migrated: false
I/flutter ( 8314): │ 🐛 migrated: true

Here is how the builder decides whether it can show the contents based on the state of the migration:

@override
  Widget build(BuildContext context) {
    bool show = false;

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: migrated
          ? Container(
              padding: const EdgeInsets.all(20.0),
              child: FutureBuilder(
.
.
.

I think this code is unimportant because the problem appears to be that "migrated" is not being set to true prior to the hang.

-Dan

hortigado commented 3 months ago

You can try use await await Repository().initialize(); the first time late more because it create the base. Although you say that you are doing it synchronously, there must be some error in the process because that error only appears when you call the repository and it has not yet started. Also how you put the token of the new user login? or you not use it. For example i put it in the method Repository.configure(AppWrite.endpoint, token);

tshedor commented 3 months ago

@SubutaDan based on the code you've provided, there's a chance for a race condition since you're not using the await keyword. I don't have the full picture, but this is the best explanation I can think of.

// This destroys and closes the SQLite db
repository.reset().then((_) => {}}

// This accesses a database that's potentially mid-destruction since it's reset is not guaranteed
repository.initialize()

Instead, consider a repository function that does it all

class Repository {
  Future<void> recreate() async {
    await reset();
    await initialize();
  }
}
SubutaDan commented 3 months ago

Thank you both for your suggestions, @tshedor and @hortigado .

This problem seems to have nothing to do with Brick. It appears instead to be the result of navigating to the list screen by returning a widget instead of using a Navigator method.

Sorry to have wasted your time, and thanks again for the help.

SubutaDan commented 3 months ago

Closing as unrelated to Brick.