12

I'm trying to create a server-side Dart class that performs various data-related tasks. All of these tasks rely on the database having been first initialized. The problem is that the init of the database happens asynchronously (returns a Future). I first tried to put the init code into the constructor, but have given up on this approach as it seems to not be viable.

I am now attempting to figure out how to force the DB initialization as a first step in any method call that accesses data. So in other words, when attemptLogin() is called below, I'd like to first check if the DB has been initialized and initialize it if necessary.

However, there are two obstacles. If the database hasn't been initialized, the code is straightforward - initialize the db, then use the then() method of the returned future to do the rest of the function. If the db is not yet initialized, what do I attach my then() method to?

Second related question is what happens when a database is currently being initialized but this process is not yet complete? How can I pull in and return this "in-progress" Future?

This is the basic gist of the code I'm trying to wrangle:

class DataManager {
  bool DbIsReady = false;
  bool InitializingDb = false;
  Db _db;

  Future InitMongoDB() {
    print("Initializing MongoDB");
    InitializingDb = true;
    _db = new Db("mongodb://127.0.0.1/test");
    return _db.open().then((_) {
      DbIsReady = true;
      InitializingDb = false;
    });
  }  

  Future<List> attemptLogin(String username, String password) {
    Future firstStep;
    if ((!DbIsReady) && (!InitializingDb) {
       Future firstStep = InitMongoDB()
    }
    else if (InitializingDb) {
       // Need to return the InitMongoDB() Future that's currently running, but how?
    }
    else {
       // How do I create a blank firstStep here? 
    }

    return firstStep.then((_) {
      users = _db.collection("users");
      return // ... rest of code cut out for clarity
    });
  }
}

Thanks in advance for your help,

Greg

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
Greg Sherman
  • 1,084
  • 1
  • 11
  • 22
  • I would find it easier to ensure DB initialization before any DB related code can run not before each DB operation. Is there a reason you do it this way? – Günter Zöchbauer Jul 10 '14 at 14:16
  • @Günter I initially tried to do the DB init in the class constructor, but ran into problems with testing. I posted a question about it on StackOverflow here: http://stackoverflow.com/questions/24592070/waiting-for-my-class-to-initialize-or-how-to-wait-for-a-future-to-complete , but got no good answers on how to solve the problem. – Greg Sherman Jul 10 '14 at 14:28
  • I added an answer to your other question. I haven't tested it but at least it should help you to get the idea. – Günter Zöchbauer Jul 10 '14 at 14:37

6 Answers6

18

Just return

return new Future<bool>.value(true); 
// or any other value instead of `true` you want to return.
// or none
// return new Future.value();
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
3

Just keep the future alive:

class DataManager {
  Future _initializedDb;

  Future initMongoDb() { ... }

  Future<List> attemptLogin(String username, String password) {
    if (_initializedDb == null) {
       _initializedDb = initMongoDB();
    }
    return _initializedDb.then((db) {
      users = db.collection("users");
      return // ... rest of code cut out for clarity
    });
  }
}

You might need to pay attention for the error-case. It's up to you if you want to deal with errors in the initMongoDB or after it.

Florian Loitsch
  • 7,698
  • 25
  • 30
  • Are you saying that you can chain to a completed future, or a future mid-execution? I haven't seen this in any examples, and there are no docs for this. – Greg Sherman Jul 10 '14 at 22:47
  • As a user of a future you never know if the value is already present or not. So this is normal behavior. However, futures must have at least one listener when there is an error. Otherwise the error is considered unhandled and might bring down your program. This means that you should always add at least an error handler as soon as possible. – Florian Loitsch Jul 11 '14 at 08:10
3

For those that are still wondering how to create a blank Future in Dart and later complete them, you should use the Completer class like in the next example.

class AsyncOperation {
  final Completer _completer = new Completer();

  Future<T> doOperation() {
    _startOperation();
    return _completer.future; // Send future object back to client.   
  }

  // Something calls this when the value is ready.   
  void finishOperation(T result) {
    _completer.complete(result);   
  }

  // If something goes wrong, call this.   
  void _errorHappened(error) {
      _completer.completeError(error);   
  }
}
RodXander
  • 643
  • 7
  • 12
2

One of the possible solutions:

import "dart:async";

void main() {
  var dm = new DataManager();
  var selectOne = dm.execute("SELECT 1");
  var selectUsers = dm.execute("SELECT * FROM users");
  var users = selectOne.then((result) {
    print(result);
    return selectUsers.then((result) {
      print(result);
    });
  });

  users.then((result) {
    print("Goodbye");
  });
}

class Event {
  List<Function> _actions = new List<Function>();
  bool _raised = false;

  void add(Function action) {
    if (_raised) {
      action();
    } else {
      _actions.add(action);
    }
  }

  void raise() {
    _raised = true;
    _notify();
  }

  void _notify() {
    if (_actions.isEmpty) {
      return;
    }

    var actions = _actions.toList();
    _actions.clear();
    for (var action in actions) {
      action();
    }
  }
}

class DataManager {
  static const int _STATE_NOT_INITIALIZED = 1;
  static const int _STATE_INITIALIZING = 2;
  static const int _STATE_READY = 3;

  Event _initEvent = new Event();
  int _state = _STATE_NOT_INITIALIZED;

  Future _init() {
    if (_state == _STATE_NOT_INITIALIZED) {
      _state = _STATE_INITIALIZING;
      print("Initializing...");
      return new Future(() {
        print("Initialized");
        _state = _STATE_READY;
        _initEvent.raise();
      });
    } else if (_state == _STATE_INITIALIZING) {
      print("Waiting until initialized");
      var completer = new Completer();
      _initEvent.add(() => completer.complete());
      return completer.future;
    }

    return new Future.value();
  }

  Future execute(String query, [Map arguments]) {
    return _init().then((result) {
      return _execute(query, arguments);
    });
  }

  Future _execute(String query, Map arguments) {
    return new Future.value("query: $query");
  }
}

Output:

Initializing...
Waiting until initialized
Initialized
query: SELECT 1
query: SELECT * FROM users
Goodbye

I think that exist better solution but this just an attempt to answer on your question (if I correctly understand you).

P.S. EDITED at 11 July 2014

Slightly modified (with error handling) example.

import "dart:async";

void main() {
  var dm = new DataManager();
  var selectOne = dm.execute("SELECT 1");
  var selectUsers = dm.execute("SELECT * FROM users");
  var users = selectOne.then((result) {
    print(result);
    return selectUsers.then((result) {
      print(result);
    });
  });

  users.then((result) {
    print("Goodbye");
  });
}

class DataManager {
  static const int _STATE_NOT_INITIALIZED = 1;
  static const int _STATE_INITIALIZING = 2;
  static const int _STATE_READY = 3;
  static const int _STATE_FAILURE = 4;

  Completer _initEvent = new Completer();
  int _state = _STATE_NOT_INITIALIZED;

  Future _ensureInitialized() {
    switch (_state) {
      case _STATE_NOT_INITIALIZED:
        _state = _STATE_INITIALIZING;
        print("Initializing...");
        new Future(() {
          print("Initialized");
          _state = _STATE_READY;
          // throw null;
          _initEvent.complete();
        }).catchError((e, s) {
          print("Failure");
          _initEvent.completeError(e, s);
        });

        break;    
      case _STATE_INITIALIZING:
        print("Waiting until initialized");
        break;
      case _STATE_FAILURE:
        print("Failure detected");
        break;
      default:
        print("Aleady intialized");
        break;
    }

    return _initEvent.future;
  }

  Future execute(String query, [Map arguments]) {
    return _ensureInitialized().then((result) {
      return _execute(query, arguments);
    });
  }

  Future _execute(String query, Map arguments) {
    return new Future.value("query: $query");
  }
}
mezoni
  • 10,684
  • 4
  • 32
  • 54
1

Future<Type> is non nullable in Dart, meaning that you have to initialize it to a value. If you don't, Dart throws the following error:

Error: Field should be initialized because its type 'Future<Type>' doesn't allow null.

To initialize a Future<Type>, see the following example:

Future<String> myFutureString = Future(() => "Future String");

Here "Future String" is a String and so the code above returns an instance of Future<String>.

So coming to the question of how to create a blank/empty Future, I used the following code for initializing an empty Future List.

Future<List> myFutureList = Future(() => []);

I found this link to be quite useful in understanding Futures in Flutter and Dart: https://meysam-mahfouzi.medium.com/understanding-future-in-dart-3c3eea5a22fb

Bhumika Sethi
  • 81
  • 1
  • 2
0

To create a black Future of type void, for instance:

onRefresh: () async {},

Here, a blank function of type Future<void> assigned to onRefresh property.

rozerro
  • 5,787
  • 9
  • 46
  • 94