Look, the Flutter SDK docs do not go far enough into state management in that Google actually wants us using MVVM state management solutions since the actual framework itself is in fact not MVC but MVVM,
This an easy way to got mobx state events for debugging.
The Logging Singleton
First, we need a logging singleton:
// Copyright 2023 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// ignore_for_file: unused_local_variable | |
import 'dart:developer'; | |
import 'package:logging/logging.dart'; | |
import 'package:logging_appenders/logging_appenders.dart'; | |
/// The app Global logger. | |
final appLogger = Logger("LoggingDemo"); | |
/// AppLogging class initializes the logging set up at object creation. | |
/// To call it is just AppLogging() within the main function block. | |
/// | |
/// Note, this an obseverable log that operates in all modes including | |
/// release which means we do not use print but instead use log to output. | |
/// | |
/// @author Fredrick Allan Grott. | |
class AppLogging { | |
factory AppLogging() { | |
return _appLogging; | |
} | |
AppLogging._internal() { | |
_init(); | |
} | |
static final AppLogging _appLogging = AppLogging._internal(); | |
void _init() { | |
// disable hierarchical logger | |
hierarchicalLoggingEnabled = false; | |
Logger.root.level = Level.ALL; | |
// stack traces are expensive so we turn this on for | |
// severe and above | |
recordStackTraceAtLevel = Level.SEVERE; | |
// just so during a log level change that we know about it. | |
// log is used instead of print as this is for observable | |
// logging even in release mode | |
Logger.root.onLevelChanged.listen((event) { | |
log('${event?.name} changed'); | |
}); | |
// now to get the log output | |
Logger.root.onRecord.listen((record) { | |
if (record.error != null && record.stackTrace != null) { | |
log( | |
'${record.level.name}: ${record.loggerName}: ${record.time}: ${record.message}: ${record.error}: ${record.stackTrace}', | |
); | |
log( | |
'level: ${record.level.name} loggerName: ${record.loggerName} time: ${record.time} message: ${record.message} error: ${record.error} exception: ${record.stackTrace}', | |
); | |
} else if (record.error != null) { | |
log( | |
'level: ${record.level.name} loggerName: ${record.loggerName} time: ${record.time} message: ${record.message} error: ${record.error}', | |
); | |
} else { | |
log( | |
'level: ${record.level.name} loggerName: ${record.loggerName} time: ${record.time} message: ${record.message}', | |
); | |
} | |
}); | |
// Appenders set up for color logs | |
PrintAppender(formatter: const ColorFormatter()).attachToLogger(Logger.root); | |
// Any appendeing logs to 3rd party observable logging services | |
// would be here using the logging appenders other appenders | |
} | |
} |
The way print and debugPrint worked was to print things to the console. The problem with that is in desktop apps you might not want that to happen. Log by contrast is a no-op unless the devTools is running and so that is why we us it as you cannot good it up as it's idiot-proof as far app user privacy and security.
MobX sets up a global mainContext, and we can then use that to set up global logging of state events that only shows up in a console when devTools is operating, which will be during debug and profiling:
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// ignore_for_file: avoid_print | |
import 'package:flutter/material.dart'; | |
import 'package:mobx/mobx.dart'; | |
import 'package:mobx_logging/src/app_logging.dart'; | |
import 'package:mobx_logging/src/features/counter_store.dart'; | |
import 'package:mobx_logging/src/my_app.dart'; | |
import 'package:provider/provider.dart'; | |
void main() { | |
// initialize logging | |
AppLogging(); | |
mainContext.config = mainContext.config.clone( | |
isSpyEnabled: true, | |
); | |
mainContext.spy((event) { | |
appLogger.info("event name: ${event.name}"); | |
appLogger.info("event type:${event.type}"); | |
}); | |
runApp(MultiProvider( | |
providers: [ | |
Provider<Counter>(create: (_) => Counter()), | |
], | |
child: const MyApp(), | |
)); | |
} |
AppLogging is called first to initialize the logging singleton. Then I call the config of mainContext in order to turn the spy logging on. Then I feed the spy state events back into the logging system via the appLogger.info calls.
Side Note: I am showing the simplified provider settings. In a real world app I would be using ProxyProvider to load the root app store in case of multiple stores and so that I could load services attached to those stores. Then I would use the ConsumerN constructs to load both the store and services into the widget tree.
Now, if instead you were using non-widget DI you would then be implementing connected dependency DI loading where one DI-dependency is dependent on another dependency. And the main reason we have that handling of services in our DI sequence is that in MVVM we have the view-model act as the usecase and that is where we are loading services such as an API repository to an internet based API for example. Another example of a service to load this way is the user preferences from local storage.