Flutter: "Binding has not yet been initialized" when accessing Flutter Secure Storage from an isolate - flutter

I'm working on a large Flutter project for Android/iOS/Windows. It will have multiple processor-intensive code segments, such as server data syncing, so to prevent the UI from being slowed down we're running those on separate isolates. One of the packages we're using is Flutter Secure Storage 7.0.0 to store data between sessions. However, whenever I try to access secure storage from within the isolate, I get the following error:
Binding has not yet been initialized.
The "instance" getter on the ServicesBinding binding mixin is only available once that binding has been initialized.
Typically, this is done by calling "WidgetsFlutterBinding.ensureInitialized()" or "runApp()" (the latter calls the former). Typically this call is done in the "void main()" method. The "ensureInitialized" method is idempotent; calling it multiple times is not harmful. After calling that method, the "instance" getter will return the binding.
In a test, one can call "TestWidgetsFlutterBinding.ensureInitialized()" as the first line in the test's "main()" method to initialize the binding.
If ServicesBinding is a custom binding mixin, there must also be a custom binding class, like WidgetsFlutterBinding, but that mixes in the selected binding, and that is the class that must be constructed before using the "instance" getter.
The error suggests calling WidgetsFlutterBinding.ensureInitialized(), however I've added that to the main function of my main isolate, and attempting to call it on the second isolate throws another error about running UI actions on non-root isolates.
One of the possible workarounds I'm aware of is to use send/receive ports to only use secure storage on the main isolate, and transfer data as needed. The problem is, one of the future plans for these isolates is to keep these isolates alive when the user closes the app, to allow us continuous background data syncing. My team has little experience on that kind of isolate usage, so I'm aware I may have a misunderstanding of how that part of our app may work in the future. But until we have a better grasp of that, we're looking to make the isolates as independent from the main isolate as possible.
Here's a minimal code sample needed to reproduce this issue:
//Run this function from main isolate
void runIsolateStorageTest() async {
//Create storage and write a value
const storage = FlutterSecureStorage();
await storage.write(key: "key", value: "value");
//Create and run second isolate. Main project uses a send/recieve port system, so I've replicated that here
var recievePort = ReceivePort();
await Isolate.spawn(isolateMain, recievePort.sendPort);
//Await the returned value from second isolate and print to console
var storageValue = await recievePort.first;
print(storageValue);
}
//Function to be run on second isolate
void isolateMain(SendPort sendPort) async {
//Several S.O. posts on similar issues have recommended this line. However, it seems to have no effect on my code
DartPluginRegistrant.ensureInitialized();
//Create storage and attempt to access written key
const storage = FlutterSecureStorage();
dynamic value = "";
try {
//Error is thrown when attempting to read value.
//Other storage functions such as .write or .containsKey throw the same error when used here
value = await storage.read(key: "key");
}
on Error catch (e) {
value = e;
}
//Return the resulting value to the main isolate
sendPort.send(value);
}
EDIT: After even more searching and digging through the flutter_secure_storage code, I've been able to more accurately identify the problem; This package uses platform channels to handle the platform specific storage implementations. According to the Flutter docs, channel methods must be invoked on the main thread. There may be a way around that using platform specific implementations, but I haven't figured that out yet.

Related

Forcing the MobX state to be changed through declared actions only in the Flutter framework

I'm experimenting with MobX state management solution in Flutter. I really like its conciseness and its simplicity but I'm a bit concerned in the drawbacks its way of mutating the state can introduce.
Theoretically , we can force the state to be mutated only using actions and this is great because we can have a history of the changes using the spy feature the package provides.
However, actions are automatically created by the framework if we do not use a specific one. For example, in the code below, I created a counter store class and I used it in the main function to assign a new value for the counter without using a predeclared action. The stric-mode has been previously set to "always".
abstract class _Counter with Store {
#observable
int value = 0;
#action
void increment() {
value++;
}
}
final counter = Counter(); // Instantiate the store
void main() {
mainContext.config = mainContext.config
.clone(isSpyEnabled: true, writePolicy: ReactiveWritePolicy.always);
mainContext.spy((event) {
print("event name : " + event.name);
print("event type : " + event.type);
});
Counter counter = Counter();
counter.value = 10;
}
I was expecting an error to raise saying that it is not possible to change the state outside of action but this does not happen because an ad hoc action is created , called value_set, automatically. I'm wondering ,then, which is the point of forcing to use actions if it is possible to avoid using them. I mean, if we directly change the state, an action is created automatically and seen by the spy functionality making all the process more robust and predictable but, what if I want the state to be changed only by actions I previously implemented? Is there a way of doing so? For example, in the counter code I just provided , is there a way of making the counter incrementalbe only by 1? Because, in the way Mobx works, I could write everywhere in the code something like
counter.value = 10
and this will work just fine without any problems. Forcing people to use preset actions could increase the predicatability a lot and facilitate the teamwork too I think.
I tried to make the value variable private but it still remains accessible from the outside , also if annotated with #readonly.
Hm, that looks weird for me too, because it does work differently in MobX JS (exactly like you describing), but it seems that Dart MobX changed behaviour for single field values and they are automatically wrapped in actions now, yes.
Maybe there should be a optional rule to turn strict checking on again, it would make sense. I suggest you to create and issue or discussion on Dart MobX Github.
More info there: https://github.com/mobxjs/mobx.dart/issues/206
At the end, I found out that the behaviour I was looking for was obtainable with the #readonly annotation. Marking a variable with #readonly annotation avoids automatically creating its setter and getter methods. Moreover it requires the annotated variable to be private, denying the variable to be set directly.
Note: By the way, the way the Dart language is implemented, it is necessary to locate the Counter in a separate file to provide the desired behaviour, otherwise its private fields would be accessible anyway.

How to copy a Map in dart without getting Reference of the old one

I have a map that i get from my service and i want to copy it into 2 new Maps without being referenced and every time i change a value into one, value change to other too.
This is the Initial Map from my service config.UserGroups and i copy it like this
SavedSettings.UserGroups = new Map.from(config.UserGroups);
UnSavedSettings.UserGroups = new Map.from(config.UserGroups);
This Map is dynamic but it has String,object
Do we have an easy way to bypass reference;
What you are asking for is called deep copying and is something you need to implement yourself for your data structures since List and Map just contains references to objects. So when you are making a copy of a List or Map, you are copying the references and not the objects inside the collection.
So the correct solution would be to make some logic which allows you to make copies of UserGroup objects and put these into your new list.
But.... if you are not scared of hacky solutions....
Section with hacky solution
If you really want to have some way to get deep copy from the Dart language (and don't care if the solution is kinda hacky) it is in theory possible by make use of the idea that we can send objects to isolates which are deep copied if possible.
So we can create an ReceivePort and send our object to our self. An example of this can be seen in this example:
class MyObject {
int value;
MyObject(this.value);
#override
String toString() => 'MyObject($value)';
}
Future<void> main() async {
final list1 = [MyObject(5)];
final list2 = await hackyDeepCopy(list1);
list2[0].value = 10;
print(list1); // [MyObject(5)]
print(list2); // [MyObject(10)]
}
Future<T> hackyDeepCopy<T>(T object) async =>
await (ReceivePort()..sendPort.send(object)).first as T;
This "solution"/hack comes with some limitations when it comes to data we can copy:
The content of message can be:
Null
bool
int
double
String
List or Map (whose elements are any of these)
TransferableTypedData
SendPort
Capability
In the special circumstances when two isolates share the same code and
are running in the same process (e.g. isolates created via
Isolate.spawn), it is also possible to send object instances (which
would be copied in the process). This is currently only supported by
the Dart Native platform.
https://api.dart.dev/stable/2.14.4/dart-isolate/SendPort/send.html
It is the "In the special circumstances when two isolates share the same code and are running in the same process" part we makes use of in hackyDeepCopy which allows us to also deep copy custom made objects.
Another limitation is that hackyDeepCopy needs to be async which is kinda annoying if your code is not already async.
And lastly, this only really works on native platforms (so no Dart-running-as JavaScript stuff).

Flutter & Dart: What is the correct way to manage obtained data from a computation made in an Isolate?

When doing expensive computations in Dart it is highly recommended to start up additional isolates. I know that, since isolates don't share any state, if you want to create communication between them, there is the possibility to pass a message from one to another by using SendPort and ReceivePort. However, when doing a computation in another Isolate, within the process of this new created isolate one could actually initialize an already created variable, for instance a variable of a Singleton, right?
So my questions are: Is it okay to proceed like this? What is the correct way to manage obtained data from a computation made in an isolate and why? Should we always only work with messages when doing computations within new isolates?
No, you cannot initialize a variable of one isolate from a different isolate. They don't have shared memory as you alluded to. The only method of communication between isolates is through ports.
Even though the analyzer may allow to you to modify a variable in one isolate from another, you won't get the behavior you expect. Isolates can be thought of as entirely different processes.
I've made an example to show this behavior:
import 'dart:isolate';
Future<void> main(List<String> arguments) async {
final single = Singleton();
print(single.variable);
single.variable = 1;
print(single.variable);
final port = ReceivePort();
final isolate = await Isolate.spawn(isolateWork, port.sendPort);
await for(dynamic message in port) {
if(message == 'done') {
isolate.kill();
break;
}
}
}
void isolateWork(SendPort port) {
final single = Singleton();
print('In isolate: ${single.variable}');
port.send('done');
}
class Singleton {
int variable = 0;
static final Singleton _singleton = Singleton._internal();
factory Singleton() {
return _singleton;
}
Singleton._internal();
}
This is a simple program that attempts to use a singleton to modify a variable in one isolate from a spawned one. If there were shared memory, one might expect the following output as I first obtain the singleton, print the default variable value of 0, change variable to 1, and then print variable in the isolate:
0
1
In isolate: 1
However, since these isolates are completely separate processes, the spawned isolate has its own instance of the singleton, leading to the following output:
0
1
In isolate: 0
0 and 1 are printed as expected in the main isolate, but the spawned one has its own memory and uses a separate singleton object, so it prints 0 even though it's 1 in the main isolate.
As you can see in my example above, I did use ports so that my program could detect when the isolate was finished and gracefully kill the process. Passing messages with ports are the proper and only method of passing data between with isolates.
You tagged this question as flutter, so I'm guessing you're using Flutter. If you're doing a single, large computation with a single output, using Flutter's compute method is probably the easiest way to use isolates as it removes the need for you to work with ports. However, it can be limiting and it's often useful to use a full isolate implementation like I did above.

Third Party Lib disposes context

Without the first two lines of the following, the Email is saved to the database just fine.
However, when I send the email using SendGrid's lib, an exception is thrown when the repository tries to save it, as the Context has been disposed.
System.ObjectDisposedException
It makes no sense to me unless the library is somehow mishandling threads or some such.
var response = await this._emailSender.SendEmailAsync(email);
email.ResponseStatusCode = (int)response.StatusCode;
this._emailRepository.Save(email);
My workaround is to create a new context:
var context = new ApplicationDbContext(this._options, this._applicationUserProvider);
context.Add(email);
context.SaveChanges();
Is there a better way to resolve this?
unless the library is somehow mishandling threads or some such.
It's an async method. The line after await may run on a different thread at a later time.
The place to look is in the calling code. If you call the method containing this line and don't wait/await the returned task, then the calling code can Dispose your DbContext while the email-sending Task is still running.

dart js-interop FunctionProxy callback relationship to js.context

I have a Dart js-interop callback that in turn takes a javascript callback as an argument. The dart callback implementation looks like this:
void callBackToDartCode(String query, js.FunctionProxy completionCallback) {
js.context.completionCallback = completionCallback;
doSomethingAscyn(query).then(
(result) {
// hand the query result back to the javascript code
js.context.completionCallback(js.map(result));
});
This works. The key to making this work is to save the FunctionProxy in the js.context so that it is available when it comes time to execute it in the async "then" method. This line of code is important:
js.context.completionCallback = completionCallback;
If that's not done then the completeCallback is not retained and hence cannot be called when the async operation completes.
I have not seen examples like this and I am not sure I have really done this properly.
It raises questions:
How do I disassociate "completeCallback" from js.context after I've called it? Does it remain associated with js.context forever?
It appears there will be conflicting use of the name "completionCallback" within js.context if multiple async operations are in progress at the same time. That strikes me as a common problem. Does js-interop have a way to deal with that or is it my job to manage that?
With js-interop all proxies are scoped to prevent memory leaks. This means that Proxy will lost its JS object reference at the end of its associated scope. If scoped((){}) function is not use explicitely a lazy scope is initialized the first time an interop operation is done and the scope is automatically closed at the end of the current event loop. If you want to make a Proxy to live longer than its associated scope, you have to retain it. This can be done with js.retain(proxy). Once your proxy is no longer needed, you can release it with js.release(proxy).
Thus your code should be :
void callBackToDartCode(String query, js.FunctionProxy completionCallback) {
js.retain(completionCallback);
doSomethingAscyn(query).then(
(result) {
// hand the query result back to the javascript code
completionCallback(js.map(result));
// completionCallback is no longer used
js.release(completionCallback);
});
}
About your question about disassociate "completeCallback" from js.context you could have done it with js.deleteProperty(js.context, "completeCallback")