Best Practice

Here are a few tips for using Scope in no particular order.


Not a substitute for Provider et al

Scope is not a substitute for the likes of Provider which works to provide values for your Flutter BuildContext. The Flutter build method isn't called on your stack (as it's called by the Flutter framework) and Scope only works for methods calls nested within the Scope's run method.

Place ScopeKeys in their own library

ScopeKeys need to be visible at the point where you inject a value and where you use the value.

Place your ScopeKeys in a separate Dart library to make them easy to import where they are needed.

import 'package:scope/scope.dart';
final ageKey = ScopeKey<int>();
final nameKey = ScopeKey<String>();
final monthKey = ScopeKey<String>();
final dbKey = ScopeKey<Db>();

Use scope when you have to pass values down

Scope is intended for use cases where you create some resource in a top level method and then need access to that resource way down in your call hierarchy and you don't want to have to pass the value down as an argument to each method.

Consider Scope for server side apps

Scope works great for Server Side and Cli apps.

Use a Scope to hold a Session object when servicing a http request or a database connection.

Use Scope in Flutter apps

Scope is not a replacement for the likes of Provider and Bloc. Provider and Bloc provide DI for you widget build methods.

Scope still has a part to play in Flutter apps. If you are using a database or making network calls from your Flutter app then Scope can be a useful tool in your kit.

Used debugName

Both Scope and ScopeKey allow you to passing in a argument debugName. The debugName is included in Exceptions which can make it much easier to find the particular Scope or ScopeKey that is the source of the problem.

static final ScopeKey<int> tenantIdKey = ScopeKey<int>('tenantIdKey');
Scope('withTenant')
..value<int>(Tenant.tenantIdKey, tenantId)
..run(() {
  action();
});  

For ScopeKey's use the name of the key. If you are a package developer prefix it with the name of your package e.g. scope.tenantIdKey

For Scope's use the name of the method or class that create the scope.

The key thing is that when you get an exception there is enough information to identify the Scope or ScopeKey.

Replace Singletons

A Scope is often a good replacement for a Singleton.

Use Scope in unit tests

Scope works well when building unit tests. The Dart test package is able to run tests concurrently. A Scope lets each unit tests hold its own set of values. Deeply nested methods can check if a ScopeKey exists via Scope.hasScopeKey and modify their behaviour. This can be easier than setting up an entire mock framework.

Use Scope just about everywhere

You can call use() inside class constructors, in individual methods or even top-level functions outside of any class.

The only criteria is that when you call 'use' a Scope can be found in some parent method/function somewhere on the call stack.

Document your injected values

Other people can't see what values you inject into a Scope. Make sure to explicitly list all dependencies of your public API in its doc comments!

Create Wrapper classes

To make your Scope easier to use create specialised wrapper functions or classes that use a nomenclature specific to the usage domain.

In this example we wrap a Scope in a Transaction class.

A user creates a Transaction class that obtains a Db connection which the user can use anywhere within the scope of the Transaction by calling Transaction.db.

import 'package:scope/scope.dart' as scope;

final dbKey = ScopeKey<Db>();

// provide a transacition class that acquires a db cnnection
class Transaction
{
     void run(void Function() action)
     {
          var db = DbPool.acquire();
          Scope()
             ..value(dbKey, db)
             ..run(action);
           DbPool.release(db);
     }
    
     /// To access the db inscope for this transaction
     static DB db() => use(dbKey);
}

/// Use the transaction class
void main() 
{
     Transaction().run() {
          createUser();
        });
 }
 
 void createUser() {
       /// get the db from transaction
       var db = Transation.db();
       db.insertUser();
}

This way, a consumer of your package doesn't have to look up what ScopeKeys are, and gets a type-safe list of all of your public dependencies at a glance.

Last updated