slim 4.2.2

  • Readme
  • Changelog
  • Example
  • Installing
  • 82

slim - app essentials #

slim was written to give some app essentials and capabilities that are common for most apps. slim makes it easier to set up app infrastructure so you can start working on your screens and logic.

  • Localizations
  • UI Messages
  • State Management
  • AppLifecycleState Events
  • Useful Extensions
  • Rest Api

Configurations #

Easy app level configuration that gives 'ready to use' localization and UI messages.

  1. App class constructor This is the place to set your supported locales and if you want also set your own SlimLocaleLoader The default SlimLocaleLoader will load you locale files from - assets/locales/ so make sure you created folder and files matching your supported locales and add it to pubspec.yaml file.

  2. MaterialApp builder Add the SlimMaterialAppBuilder.builder to it. If you have additional builders just chain them. SlimMaterialAppBuilder.builder gives you the UI messages caps.

  3. MaterialApp localizationsDelegates Set to SlimLocalizations.delegates That will support all delegates needed including the SlimLocalizations.slimLocaleLoader delegate.

  4. MaterialApp supportedLocales Set to SlimLocalizations.supportedLocales you configured in App class constructor.

The following example will load simple json locale configurations from - assets/locales/en.json

import 'package:slim/slim.dart';

class MyApp extends StatelessWidget{
	MyApp(){
		SlimLocalizations.supportedLocales = [Locale('en', 'US')];
		/// If you want to customize you locale loader just create class that extends SlimLocaleLoader and change:
    ///
		/// SlimLocalizations.slimLocaleLoader= YouCustomLocalLoader();
	}

	@override
	Widget build(BuildContext context) =>
		   MaterialApp(
				builder: SlimMaterialAppBuilder.builder,
				title: 'Flutter Demo',
				theme: ThemeData(
				primarySwatch: Colors.blue,
				visualDensity: VisualDensity.adaptivePlatformDensity,
				),
				home: MyHomePage("First Screen"),
				localizationsDelegates: SlimLocalizations.delegates,
				supportedLocales: SlimLocalizations.supportedLocales,
			);
}

Localization #

UI direction
The confgurations above will make your UI ltr/rtl automatically according to os locale. You can use BuildContext extension method to get your locale text direction context.textDirection

Translation
If you didn't provide any custom SlimLocaleLoader , Your localization would be configured with a default one. The default SlimLocaleLoader expects locale files to be in - assets/locales/ folder with name convention of your locale. for example - assets/locales/en.json. The default locale file format is a single level json that holds you translation. You can extends SlimLocaleLoader to your own.

{
  "welcome": "translation of 'welcome' key"
}

Access to translation of a key gained by BuildContext extension method called translate.

@override
Widget build(BuildContext context) => Text(context.translate('welcome'));

UI Messages #

UI Messages caps are available via BuildContext extension methods. Since SlimController accessed by SlimBuilder has an access to current context, these caps are also available inside it. That will be explained in the State Management section below. You can display overlay with your own widget, text message and a snackbar.

showWidget(Widget widget, {bool dissmisble = true, Color overlayColor = Colors.black,double overlayOpacity = .6})

showOverlay(String message,{Color backgroundColor = Colors.black,
bool dissmisble = true,textStyle = const  TextStyle(color: Colors.white), Color overlayColor = Colors.black,double overlayOpacity = .6});

showSnackBar(String message,{Color messageBackgroundColor = Colors.black,
messageTextStyle = const  TextStyle(color: Colors.white)});

You can use each one of the above via BuildContext extension methods. for example:

@override
Widget build(BuildContext context) => Column(children:[
		 FlatButton(
			 child:Text('show message'),
			 onPressed:() => context.showOverlay("some text"),
		),
		FlatButton(
			 child:Text('show snack bar'),
			 onPressed:() => context.showSnackBar("some text"),
		),
		FlatButton(
			 child:Text('show widget'),
			 onPressed:() => context.showWidget(
                 Container(
				    height: 100,
					widgth: 100,
					color: Colors.black
				),
            ),
		),
	],
);

For text overlay and snackbar you can set the background color and text style.
For overlay of text or widget you can specify if dismissible.

State Management #

slim provides state management based on InhertiedNotifier and ChangeNotifier classes. The concept is that you keep an object at the top of the tree or sub tree of children that might access it. The state object can be a widget state object, data object, business object or whatever you want it to be. You can even use it for passing parameters between screens, so it can be a String, int or even an enum. This is the one of the powerful concepts of slim state management - it can be any state and not limited for a widget state.

Before you go over below examples, excluding the case of parameter passing between screens, the most recommended state object is the SlimController and recommended way to access it is via SlimBuilder widget.
By using the SlimController from one side and SlimBuilder on the other, you get the best of three worlds:

  1. Widget state control - you can make the wrapped widget to rebuild by calling updateUI method. Since SlimController based on ChangeNotifier it will cause all widgets that referenced it to rebuild. That is great but considered to be a ChangeNotifier disadvantage in case you only want to rebuild the current widget. For that you can pass the current flag and decide when you want to globaly or localy update the UI.

    updateUI({bool current = false})

  2. Global & Local shared objects/values - you can make any object/values shared between your app screens. Global - put your object/value at the top of your app tree by wrapping the material app widget. Local - put your object/value at the top of a sub tree. That way you can share objects for a local tree or pass parameters between screens.

  3. Separate & not disconnected business logic - The SlimController accessed by SlimBuilder allows you to separate the business from UI but gives access to current context. That gives you that ability to combine navigation flows and use the UI messages inside your business class.

SlimController
abstract class that can be used for state management or logic, inherits from ChangeNotifier and gives you widget rebuild options:
T slim<slim>() - access to ancestor slims updateUI({bool current = false}) - will refresh the state of all / current widgets that reference it (current update flag workd only if you access it via [SlimBuilder] widget).
closeKeyBoard() - close keyboard by requesting focuse The SlimController has context propery to access the current context so you can use context extensions from inside a business login class interacting with UI:
showOverlay - display overlay text message
showWidget - display overlay widget
showSnackBar - display snackbar with given text
clearOverlay - clears overlays
forceClearOverlay - clears overlays even if not dismissible

For overlay message and snackbar you can set background color, text style, overlay color and overlay opacity.
For overlay message and widget you can set dismissible flag.

SlimAppStateController
abstract class that inherits from SlimController and recieves AppLifecycleState events. the events change recieves only if the SlimAppStateController is accessed in the current app's screen. SlimAppStateController force to override its void onAppStateChanged(AppLifecycleState state) method.

SlimAppStateController must be access via SlimBuilder, SlimController access via SlimBuilder is optional but recommended

Putting Slims in the tree
For the following example we will use a simple Counter class:

class Counter extends SlimController{
	int value=0;
	inc(){
		value++;
	}
}

SingleSlim:

Slim<Counter>(child:someWidget,stateObject:Counter());

SingleSlim with a Slimer:

Slimer<Counter>(Counter()).slim(someWidget);

MultiSlim with slimers:

MultiSlim(child:someWidget,slimers:[Slimer<Counter>(Counter())]);

MultiSlim via List<Slimer> extension method:

[Slimer<Counter>(Counter())].slim(child:someWidget);

Access slim objects in the tree

SlimBuilder - allows updateUI(current:true)

@override
Widget build(BuildContext context){
	return SlimBuilder<Counter>(
    [instance: optional local instance of Counter if you dont want to pre put it on tree]
		builder:(counter){
			...
			return someWidget;
		}
	);
}

Simple Access - without updateUI(current:true) support

@override
Widget build(BuildContext context){
	final counter = Slim.of<Counter>(context);
	...
	return someWidget;
}
@override
Widget build(BuildContext context){
	final counter = context.slim<Counter>();
	...
	return someWidget;
}

Useful Extensions #

slim provides some useful extension methods for several classes (some of them mentioned previously). The full extension methods list is:

String
bool isNullOrEmpty
bool isNotNullOrEmpty
String format(List<dynamic> variables)

BuildContext
bool hasOverlay - true if any overlay currently displayed
void clearOverlay() - clears current overlay message if dismissible
void forceClearOverlay() - clears current overlay message even if not dismissible
void showWidget(Widget widget, {bool dismissible = true})

void  showOverlay(String message,{Color backgroundColor = Colors.black,
bool dismissible = true,messageTextStyle = const  TextStyle(color: Colors.white), Color overlayColor = Colors.black, double overlayOpacity = .6})
void  showSnackBar(String message,{Color backgroundColor = Colors.black,
messageTextStyle = const  TextStyle(color: Colors.white), Color overlayColor = Colors.black, double overlayOpacity = .6})

T slim<T>() - access a state object of type T
double width - media query width
double height - media query height
NavigatorState navigator - navigator state
void pop<T>({T result}) - navigator pop
Future<T> push<T>(Route<T> route) - navigator push
Future<T> pushReplacement<T>(Route<T> route) - navigator pushReplacement
void popTop() - navigator pop till can't pop anymore
String translate(String key,[String group]) - locale translation of key
TextDirection textDirection - current locale text direction
void closeKeyboard() - request focuse

Widget
Future<T> push<T>(BuildContext context) - navigator push
Future<T> pushReplacement<T>(BuildContext context) - navigator pushReplacement
Future<T> pushTop<T>(BuildContext context) - push at navigators most top

int
Duration get seconds - get in seconds
Duration get hours - get in hours
Duration get days - get in days
Duration get minutes - get in minutes
Duration get milliseconds - get in milliseconds
Duration get microseconds - get in microseconds
DateTime get nowMinutesInterval - get current time with minutes round up to interval

RestApi #

SlimApi is an abstract class that gives you rest (get, delete, post, put) methods for fast service writing.
The SlimApi class constructor gets the server url, and its methods gets the service url and some additional data.
SlimApi class has a createHeaders method that can be overriden.
SlimApi methods wrapped in try/catch clause and returns SlimResponse object.

class SlimResponse {
  bool get success => statusCode == 200 || statusCode == 201;
  int statusCode;
  String body;
  String exception;
  RestApiMethod method;
  String url;
  int milliseconds;
  String get error => body.isNullOrEmpty ? exception : body;

  SlimResponse(this.url, this.method, this.statusCode, this.milliseconds);

  @override
  String toString() => "$method [$statusCode] [$error] ${milliseconds}ms";
}

Exapmle for a login service:


class LoginService extends SlimApi {
  LoginService() : super("http://myserver.com/api");

  /// POST http://myserver.com/api/login
  Future<SlimResponse> login(User user) =>
      post("login", {"userName": user.userName, "password": user.password});

  /// POST http://myserver.com/api/logout
  Future<SlimResponse> logout(User user) =>
      post("logout", {"userName": user.userName});
}

Full Example #

This final section describes a full example that combines most of slim app essentials package.
All of slim usage is remarked.

import 'package:slim/slim.dart';
  1. locale json file - assets/locales/en.json
{
  "hi": "Hello",
  "badlogin": "Bad Login",
  "goodlogin": "Ok Login",
  "badcreds": "invalid username or password",
  "loginform": "Login"
}
  1. User class
class User{
    String userName;
    String password;
}
  1. LoginService class
/// Extends SlimApi class
class LoginService extends SlimApi {
  LoginService() : super("http://myserver.com/api");

  Future<SlimResponse> login(User user) =>
      post("login", {"userName": user.userName, "password": user.password});

  Future<SlimResponse> logout(User user) =>
      post("logout", {"userName": user.userName});
}
  1. LoginBloc class - Business logic
/// Extends slim SlimController class
class LoginBloc extends SlimController {
  badLogin(User user) async {
    /// Access login service via slim
    final loginService = slim<LoginService>();
    /// Using context access to display loading indicator
    showWidget(CircularProgressIndicator(), dismissible:false);
    final result = await loginService.login(user);
    /// Using context access to clear loading indicator
    forceClearOverlay();
    /// Checking slim RestApiResult for success
    if (result.success)
      /// Using slim widget extension method to replace current screen to Home widget
      Home().pushReplacement(context);
    else
      /// Using context access to show a snackbar and locale translation
      showSnackBar(context.translate("badcreds"), backgroundColor: Colors.red);
  }

  goodLogin(User user) async {
    /// Access login service via slim
    final loginService = slim<LoginService>();
    /// Close keyboard
    closeKeyboard();
    /// Using context access to display loading indicator
    showWidget(CircularProgressIndicator(), dismissible:false);
    await loginService.login(user);
    //Using context access to clear loading indicator
    forceClearOverlay();
    /// Using slim widget extension method to replace current screen to Home widget
    Home().pushReplacement(context);
  }
}
  1. App Configurations
class MyApp extends StatelessWidget {
  MyApp() {
    /// Set slim supported locales
    SlimLocalizations.supportedLocales = [Locale('en', 'US')];
  }

  @override
  Widget build(BuildContext context) {
    return [
      /// Putting single instance of Userat the top of the tree
      Slimer<User>(User()),
      /// Putting single instance of LoginService at the top of the tree
      Slimer<LoginService>(LoginService()),
      /// Putting single instance of LoginBloc at the top of the tree
      Slimer<LoginBloc>(LoginBloc()),
    ].slim(
      child: MaterialApp(
        /// Configure material app builder for slim UI messages support
        builder: SlimMaterialAppBuilder.builder,
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: Login(),
        /// Configure slim localizations delegates
        localizationsDelegates: SlimLocalizations.delegates,
        /// Configure slim localizations supported locales
        supportedLocales: SlimLocalizations.supportedLocales,
      ),
    );
  }
}
  1. Login screen
class Login extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    /// Using slim builder to access login bloc instance
    return SlimBuilder<LoginBloc>(
      builder: (loginBloc) {
        /// Simple slim access to user instance
        final user = context.slim<User>();
        return Scaffold(
          backgroundColor: Colors.blue,
          body: Center(
            child: Container(
              /// BuildContext extension method context.width = MediaQuery.of(context).size.width
              width: context.width * 0.8,
              child: Card(
                elevation: 5,
                child: Padding(
                  padding: const EdgeInsets.all(20.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      Text(
                        /// slim locale translation
                        context.translate("loginform"),
                        style: TextStyle(
                            fontSize: 20, fontWeight: FontWeight.bold),
                      ),
                      SizedBox(height: 20),
                      TextFormField(
                        decoration: InputDecoration(
                          hintText: "Username",
                          alignLabelWithHint: true,
                        ),
                        initialValue: user.userName,
                        onChanged: (value) => user.userName = value,
                      ),
                      TextFormField(
                        decoration: InputDecoration(
                          hintText: "Password",
                          alignLabelWithHint: true,
                        ),
                        initialValue: user.password,
                        onChanged: (value) => user.password = value,
                        obscureText: true,
                      ),
                      SizedBox(height: 20),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: <Widget>[
                          FlatButton(
                            /// slim locale translation
                            child: Text(context.translate("badlogin")),
                            /// Using the bloc
                            onPressed: () => loginBloc.badLogin(user),
                            color: Colors.pink,
                          ),
                          FlatButton(
                            /// slim locale translation
                            child: Text(context.translate("goodlogin")),
                            /// Using the bloc
                            onPressed: () => loginBloc.goodLogin(user),
                            color: Colors.green,
                          )
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}
  1. Home screen
class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    /// Using slim builder to access user instance
    return SlimBuilder<User>(
      builder: (user) => Scaffold(
        backgroundColor: Colors.lightBlue,
        body: Center(
          /// Using slim locale translation and user instance
          child: Text("${context.translate("hi")} ${user.userName}"),
        ),
      ),
    );
  }
}

The Example available in example tab and git.

Would love to get comments & suggestions.

Enjoy !

[4.2.2] #

  • date format bug fix
  • remove unused imports

[4.2.1] #

  • Add bytes to SlimResponse

[4.2.0] #

  • Date extention method for DateTime.format([d,dd,m,mm,yy,yyyy,h,hh,mi,s,ss]) - fix mi format

[4.1.9] #

  • Date extention method for DateTime.format([d,dd,m,mm,yy,yyyy,h,hh,mi,s,ss])

[4.1.8] #

  • String extension method for levenshtein score
  • Date extention method for DateTime.format([d,dd,m,mm,yy,yyyy])

[4.1.7] #

  • String extension method String.format
  • BuildContext translatef method that combine String.format method
  • SlimWidget abstraction to speed SlimBuilder wrap

[4.1.6] #

Localization:

  • add optional group parameter to enable 2-level json locale file in SlimLocaleLoader and context.translate(String key, [String group]) extension method

[4.1.5] #

  • update(readme)

[4.1.4] #

  • update(example)

[4.1.3] #

breaking changes:

  • SlimObject -> SlimController
  • SlimAppStateObject -> SlimAppStateController

[4.1.2] #

  • code polish

[4.1.1] #

  • update(tests)

[4.1.0] #

  • code polish

[4.0.0] #

breaking changes:

  • RestApi -> SlimApi
  • RestApiMethod -> SlimApiMethod
  • RestApiResponse -> SlimResponse

[3.3.7] #

  • update(documentaion)

[3.3.6] #

  • update(documentaion)

[3.3.5] #

  • introducing SlimAppStateObject to extend SlimObject and getting AppLifecycleState events

[3.3.4] #

  • update(documentaion)

[3.3.3] #

  • UI Messages: breaking change: typo dissmisable -> dissmisible, clearMessage -> clearOverlay, forceClearMessage -> forceClearOverlay
  • new extension methods on int for duration and current date with round up minutes interval

[3.3.2] #

  • UI Messages: add overlayColor & overlayOpacity options when showing overlay message / widget
  • UI Messages: breaking change: messageBackgroundColor -> backgroundColor, messageTextStyle -> textStyle

[3.3.1] #

  • publish on 1.17.2 stable channel

[3.3.0] #

  • changed SlimController init & dispose methods to onInit & onDispose

[3.2.9] #

  • clear snackbar message after displaying to avaoid showing it again on hot reload

[3.2.8] #

  • adding to SlimController init & dispose methods to call in matching Slim state (can be overriden)

[3.2.7] #

  • changing Slim to StatefulWidget to keep state during hot reload

[3.2.6] #

  • add closeKeyboard() to SlimController

[3.2.5] #

  • slim access of dependOnInheritedWidgetOfExactType fallbacks to findAncestorWidgetOfExactType to enable initState access

[3.2.4] #

  • T slim

[3.2.3] #

  • forceClearMessage & clearMessage for SlimController

[3.2.2] #

  • call UI messages directly from SlimController (no need to use context extensions)
  • change slim access from dependOnInheritedWidgetOfExactType to findAncestorWidgetOfExactType to enable initState access

[3.2.1] #

documentation update

[3.2.0] #

build fix

[3.1.9] #

SlimBuilder can get instance to put above its child incase you dont want to preput it on the tree

[3.1.8] #

SlimMessage for static messages closeKeyboard context extension

[3.1.7] #

slim app essentials

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:slim/slim.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  MyApp() {
    SlimLocalizations.supportedLocales = [Locale('en', 'US')];
  }

  @override
  Widget build(BuildContext context) {
    return [
      Slimer<User>(User()),
      Slimer<LoginService>(LoginService()),
      Slimer<LoginBloc>(LoginBloc()),
    ].slim(
      child: MaterialApp(
        builder: SlimMaterialAppBuilder.builder,
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: Login(),
        localizationsDelegates: SlimLocalizations.delegates,
        supportedLocales: SlimLocalizations.supportedLocales,
      ),
    );
  }
}

class User {
  String userName;
  String password;
}

class LoginService extends SlimApi {
  LoginService() : super("http://myserver.com/api");

  Future<SlimResponse> login(User user) =>
      post("login", {"userName": user.userName, "password": user.password});

  Future<SlimResponse> logout(User user) =>
      post("logout", {"userName": user.userName});
}

class LoginBloc extends SlimController {
  badLogin(User user) async {
    final loginService = slim<LoginService>();
    showWidget(CircularProgressIndicator(), dismissible: false);
    final result = await loginService.login(user);
    forceClearOverlay();
    if (result.success)
      Home().pushReplacement(context);
    else
      context.showSnackBar(
        context.translate("badcreds"),
        backgroundColor: Colors.red,
      );
  }

  goodLogin(User user) async {
    final loginService = slim<LoginService>();
    showWidget(CircularProgressIndicator(), dismissible: false);
    await loginService.login(user);
    forceClearOverlay();
    Home().pushReplacement(context);
  }
}

class Login extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SlimBuilder<LoginBloc>(
      builder: (loginBloc) {
        final user = context.slim<User>();
        return Scaffold(
          backgroundColor: Colors.blue,
          body: Center(
            child: Container(
              width: context.width * 0.8,
              child: Card(
                elevation: 5,
                child: Padding(
                  padding: const EdgeInsets.all(20.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      Text(
                        context.translate("loginform"),
                        style: TextStyle(
                            fontSize: 20, fontWeight: FontWeight.bold),
                      ),
                      SizedBox(height: 20),
                      TextFormField(
                        decoration: InputDecoration(
                          hintText: "Username",
                          alignLabelWithHint: true,
                        ),
                        initialValue: user.userName,
                        onChanged: (value) => user.userName = value,
                      ),
                      TextFormField(
                        decoration: InputDecoration(
                          hintText: "Password",
                          alignLabelWithHint: true,
                        ),
                        initialValue: user.password,
                        onChanged: (value) => user.password = value,
                        obscureText: true,
                      ),
                      SizedBox(height: 20),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: <Widget>[
                          FlatButton(
                            child: Text(context.translate("badlogin")),
                            onPressed: () => loginBloc.badLogin(user),
                            color: Colors.pink,
                          ),
                          FlatButton(
                            child: Text(context.translate("goodlogin")),
                            onPressed: () => loginBloc.goodLogin(user),
                            color: Colors.green,
                          )
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return SlimBuilder<User>(
      builder: (user) => Scaffold(
        backgroundColor: Colors.lightBlue,
        body: Center(
          child: Text("${context.translate("hi")} ${user.userName}"),
        ),
      ),
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  slim: ^4.2.2

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:slim/slim.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
65
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
82
Learn more about scoring.

We analyzed this package on Jul 9, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.14
  • Flutter: 1.17.5

Analysis suggestions

Package not compatible with SDK dart

Because:

  • slim that is a package requiring null.

Health suggestions

Format lib/src/message.dart.

Run flutter format to format lib/src/message.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.7.0 <3.0.0
flutter 0.0.0
flutter_localizations 0.0.0
http ^0.12.1 0.12.1
Transitive dependencies
charcode 1.1.3
collection 1.14.12 1.14.13
http_parser 3.1.4
intl 0.16.1
meta 1.1.8 1.2.2
path 1.6.4 1.7.0
pedantic 1.9.0 1.9.2
sky_engine 0.0.99
source_span 1.7.0
string_scanner 1.0.5
term_glyph 1.1.0
typed_data 1.1.6 1.2.0
vector_math 2.0.8 2.1.0-nullsafety
Dev dependencies
flutter_test