Hello,
I'm developing Flutter web app using mobx (mobx: 1.0.2, flutter_mobx: 1.0.1, mobx_codegen: 1.0.2) and I'm experiencing the issue of:
Invalid argument: Maximum call stack size exceeded.
My store's code is almost identical as the github app code from repo's examples:
List<Item> items = [];
static ObservableFuture<List<Item>> emptyResponse = ObservableFuture.value([]);
@observable
ObservableFuture<List<Item>> _itemsFuture = emptyResponse;
@computed
bool get hasResults =>
_itemsFuture != emptyResponse &&
_itemsFuture.status == FutureStatus.fulfilled;
@action
Future<List<Item>> fetchItems() async {
items = [];
final future = itemsService.getAll();
_itemsFuture = ObservableFuture(future);
items = await future;
return items;
}
The problematic part seems to be the usage of @action returning a Future, because even replacing the fetchItems action with code like:
```
@action
Future> fetchItems() async {
items = [];
sleep(Duration(seconds: 1));
return items;
}
results with the same output: `Invalid argument: Maximum call stack size exceeded`. Complete removal of that @action returning Future "solves" the issue, but the app does not get the data = doesn't work.
Any code change if followed by `flutter clean` and of course `flutter packages pub run build_runner build --delete-conflicting-outputs`.
What may be done wrong here?
Cheers!
`Flutter doctor` output:
[鉁揮 Flutter (Channel beta, v1.14.6, on Mac OS X 10.14.6 18G3020, locale pl-PL)
[!] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[鉁揮 Xcode - develop for iOS and macOS (Xcode 10.3)
[鉁揮 Chrome - develop for the web
[鉁揮 Android Studio (version 3.5)
[鉁揮 IntelliJ IDEA Community Edition (version 2017.3.5)
[鉁揮 VS Code (version 1.41.1)
[鉁揮 Connected device (3 available)
```
I figured out, that making annotated fields private is not the best idea in this case.
Changing
@observable
ObservableFuture<List<Item>> _itemsFuture = emptyResponse;
to
@observable
ObservableFuture<List<Item>> itemsFuture = emptyResponse;
solved the issue, so it seems it has nothing commin with @actions and Futures.
Ah, interesting. Thanks for getting back @michalsuryntequiqo. I'm going to open this back up under a different title so we can investigate the private field issue
Is this issues fixed?
these are my dependencies:
dependencies:
mobx: ^1.1.1
flutter_mobx: ^1.1.0
provider: ^4.0.5
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^1.8.1
mobx_codegen: ^1.0.3
But I'm getting Maximum call stack size exceeded again (probably due to private variables that I prefer to continue using). Thanks for your absolute job guys
I think this is still an open issue unfortunately. This is not particular to MobX but the way the dart2js generates the JS code.
I think this is still an open issue unfortunately. This is not particular to MobX but the way the dart2js generates the JS code.
So the only way is to remove all private variables? :'(
Any progress with this?
This error can be avoided using:
`
class Counter {
Counter() {
increment = Action(_increment);
}
final _value = Observable(0);
int get value => _value.value;
set value(int newValue) => _value.value = newValue;
Action increment;
void _increment() {
_value.value++;
}
}
`
This issue seems to extend beyond just private actions. I'm getting the same error for private @observable and @computed annotations as well. Is there any progress on this?
I'm not sure if this is relevant, but here's how I deal with async items - I'm not a fan of ObservableFuture, so I roll my own BaseStore for such async items.
BaseStore - click to expand
// base_store.dart
import 'package:dio/dio.dart';
import 'package:meta/meta.dart';
import 'package:mobx/mobx.dart';
part 'base_store.g.dart';
typedef AsyncLoader<T> = Future<List<T>> Function(
{Dio apiClient, bool forceRefresh});
class BaseStore<T> = _BaseStore<T> with _$BaseStore<T>;
abstract class _BaseStore<T> with Store {
_BaseStore({
Dio apiClient,
this.debug = false,
}) : assert(debug != null),
_apiClient = apiClient;
final Dio _apiClient;
final bool debug;
bool _everLoaded = false;
@observable
bool isLoading = false;
@observable
Object loadingError;
@observable
DateTime dateLoaded;
@observable
List<T> _items = [];
List<T> get items {
warmup();
return _items;
}
@action
Future<bool> loadItems({bool forceRefresh = false}) async =>
throw UnimplementedError('implement loadItems');
@protected
void itemsHandler(List<T> items) => _items = items;
@protected
Future<bool> invokeLoader({
AsyncLoader<T> loader,
bool forceRefresh = false,
}) async {
assert(forceRefresh != null);
if (isLoading) {
await asyncWhen((_) => isLoading == false);
if (!forceRefresh) {
return loadingError != null;
}
}
isLoading = true;
loadingError = null;
try {
final result = await loader(
apiClient: _apiClient,
forceRefresh: forceRefresh,
);
itemsHandler(result ?? []);
dateLoaded = DateTime.now();
} catch (err, stack) {
loadingError = err;
if (debug && stack != null) {
print('ERR:\n$err');
print('STK:\n$stack');
}
} finally {
isLoading = false;
}
return loadingError != null;
}
Future<bool> whenNotLoading() async {
if (isLoading) {
await asyncWhen((_) => isLoading == false);
return true;
}
return false;
}
void warmup() {
if (!_everLoaded) {
_everLoaded = true;
Future(() => loadItems());
}
}
}
Then I extend it for the individual stores.
CategoriesStore - click to expand
// categories_store.dart
import 'package:dio/dio.dart';
import 'package:mobx/mobx.dart';
import '../constants.dart';
import '../models/category.dart';
import '../services/categories.dart' as api;
import 'base_store.dart';
part 'categories_store.g.dart';
class CategoriesStore = _CategoriesStore with _$CategoriesStore;
abstract class _CategoriesStore extends BaseStore<Category> with Store {
_CategoriesStore({
Dio apiClient,
bool debug = DEBUG,
}) : super(apiClient: apiClient, debug: debug);
@computed
Map<int, Category> get idsMap {
return Map.fromIterable(items, key: (dynamic item) => (item as Category).id);
}
@action
Future<bool> loadItems({bool forceRefresh = false}) => super.invokeLoader(
loader: api.loadCategories,
forceRefresh: forceRefresh,
);
}
My widgets don't rely on a Future, although there is one - they rely on MobX's reactivity. The items initially are an empty list, then a load action is issued, which changes the state of isLoading and clears any errors, then the load action completes by either setting the items to the full list, or sets the error.
I keep a single instance of CategoriesStore(), say using Provider, then my widgets use an Observer which accesses the categoriesStore.items and work with that.
The first time the items are accessed they will be loaded (be it from local cache or fetched from the network - up to the api loader). Once they are loaded the widget will reload, because the Observer will react to the new items. I use a List and not an ObservableList, since I'm not mutating the items.
When I need to wait until the data is loaded I use await whenNotLoading() or a reaction/when to check isLoading and if I need to display the error, or check if there is such a thing - I check the loadingError prop.
My RootStore class, holds these single instances of the rest of the stores and warms them up upon its instantiation.
RootStore - click to expand
class RootStore {
RootStore({
Dio apiClient,
this.debug = DEBUG,
}) : assert(debug != null),
_apiClient = apiClient;
final bool debug;
PostsStore get postsStore =>
_postsStore ??= PostsStore(apiClient: _apiClient, debug: debug);
CategoriesStore get categoriesStore =>
_categoriesStore ??= CategoriesStore(apiClient: _apiClient, debug: debug);
MediaStore get mediaStore =>
_mediaStore ??= MediaStore(apiClient: _apiClient, debug: debug);
bool get isLoading =>
postsStore.isLoading || categoriesStore.isLoading || mediaStore.isLoading;
List<Post> get posts => postsStore.items;
List<Category> get categories => categoriesStore.items;
Category categoryForId(int id) => categoriesStore.idsMap[id];
Media mediaForId(int id) => mediaStore.idsMap[id];
// --- Private
final Dio _apiClient;
PostsStore _postsStore;
CategoriesStore _categoriesStore;
MediaStore _mediaStore;
bool _everLoaded = false;
void warmup() {
if (!_everLoaded) {
_everLoaded = true;
postsStore.warmup();
categoriesStore.warmup();
mediaStore.warmup();
}
}
}
App - click to expand
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
// NOTE: Creates the list of available providers
// They are lazily created on their very first request via
// `Provider.of<XYZ>(context)`.
return MultiProvider(
providers: [
Provider<RootStore>(create: (_) => RootStore()..warmup()),
...
],
child: _buildApp(context),
);
}
...
}
I hope this helps some of you.
Has anyone reported this to the dart2js team? There really shouldn't be breaking differences between JS and the VM like this.
On another note, @computeds are also affected.
I think I've reported once in the past, but probably get some upvotes on that. Need to dig up that issue now.
The dart2js issue has been resolved, does this mean this issue is as well on 2.12.0+?
Most helpful comment
I figured out, that making annotated fields private is not the best idea in this case.
Changing
to
solved the issue, so it seems it has nothing commin with
@actionsandFutures.