Cannot call Flutter Singleton from another package

Issue

I am trying to import an asynchronous function in Flutter to handle securely storing user data. The problem is I keep getting the following error:

packages/authentication_repository/lib/src/authentication_repository.dart:64:15:
Error: Method not found: 'set'. await SecureStorageService.set(
                                      ^^^

Here is my code:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorageService {
  static SecureStorageService _intance;
  FlutterSecureStorage flutterSecureStorage;

  SecureStorageService._internal() {
    this.flutterSecureStorage = new FlutterSecureStorage();
  }

  static Future<SecureStorageService> getInstance() async {
    if (_intance == null) {
      _intance = SecureStorageService._internal();
    }
    return _intance;
  }

  Future<void> set(String key, String value) async {
    await this.flutterSecureStorage.write(key: key, value: value);
  }

  Future<String> get(String key) async {
    return await this.flutterSecureStorage.read(key: key);
  }

  Future<void> clear() async {
    await this.flutterSecureStorage.deleteAll();
  }
}

And then I import the code like follows:

import 'package:crowdplan_flutter/storage_util.dart';

...

class AuthenticationRepository {
  final _controller = StreamController<AuthenticationStatus>();
  final secureStorage = SecureStorageService.getInstance();

...

    try {
      final response = await http.post(
        url,
        headers: <String, String>{
          'Content-Type': 'application/json; charset=UTF-8',
        },
        body: jsonEncode(<String, String>{
          'email': email,
          'password': password,
          'client_id': clientId,
        }),
      );
      if (response.statusCode == 200) {
        print(response.body);
        print(json.decode(response.body)['access_token']);
        print(json.decode(response.body)['refresh_token']);
        await secureStorage.set(
            key: 'access_token',
            value: json.decode(response.body)['access_token']);
        await secureStorage.set(
            key: 'refresh_token',
            value: json.decode(response.body)['refresh_token']);
        await secureStorage.set(
            key: 'user_id', value: json.decode(response.body)['user_id']);
        _controller.add(AuthenticationStatus.authenticated);
      }
    } catch (error, stacktrace) {
      print('Exception occurred: $error stackTrace: $stacktrace');
    }
  }

My Singleton is initiated in my main.dart file like so.

void main() async {
  await SecureStorageService.getInstance();
  runApp(App(
    authenticationRepository: AuthenticationRepository(),
    userRepository: UserRepository(),
  ));
}

I am new to Flutter so this might be a new noob error.

Solution

The set method isn’t static and can’t be accessed with SecureStorageService.set

  Future<void> set(String key, String value) async {
    await this.flutterSecureStorage.write(key: key, value: value);
  }

I see in the 2nd code snippet that you’ve assigned the singleton to secureStorage.

Did you mean to access it with something like?:

  secureStorage.set()

Part 2 – Code Example

Perhaps the async getInstance() in the singleton class is tripping you up. It doesn’t need to be async (nor should it be). (In some cases you may want an async initializer instead of a constructor. See the bottom of the Example code here for a use-case.)

SecureStorageService (the singleton) gets instantiated in your main() method so inside AuthenticationRepository it’ll use that same instance and be ready to use.

class AuthenticationRepository {
  final secureStorage = SecureStorageService.getInstance;
  // ↑ will get the same instance created in main()

The code sample in the question doesn’t specify where/when the http.post method is being called, but I’m guessing it’s an initialization / setup for AuthenticationRepository so I’ve mocked up an initStorage() method inside it.

This initStorage() call will use the SecureStorageService singleton, with a call to its secureStorage.set() method.

Hopefully this example helps you spot a difference between our code samples to figure out what’s going wrong.

import 'package:flutter/material.dart';

/// Mocking FlutterSecureStorage
/// Note that the actual package FlutterSecureStorage does not have an async
/// constructor nor initializer
class FlutterSecureStorage {
  Map<String,String> data = {};

  Future<void> write({String key, String value}) async {
    data[key] = value;
  }

  Future<String> read({String key}) async {
    print('FSS read - returning value: ${data[key]}');
    return data[key];
  }
}

class SecureStorageService {
  /// for singleton ↓ instance should be final and uses private constructor
  static final SecureStorageService _instance = SecureStorageService._internal();
  FlutterSecureStorage flutterSecureStorage;

  /// Private constructor, not async
  SecureStorageService._internal() {
    flutterSecureStorage = FlutterSecureStorage();
  }

  /// This doesn't need to be async. FlutterSecureStorage (FSS) doesn't have an async initializer
  /// and constructors are generally never async
  /*static Future<SecureStorageService> getInstance() async {
    if (_instance == null) {
      _instance = SecureStorageService._internal();
    }
    return _instance;
  }*/

  /// static singleton instance getter, not async
  static SecureStorageService get getInstance => _instance;

  /// don't need "this" keyword & could use FSS methods directly, but leaving as is
  Future<void> set({String key, String value}) async {
    await flutterSecureStorage.write(key: key, value: value);
  }

  Future<String> get({String key}) async {
    return flutterSecureStorage.read(key: key);
  }
}

class Response {
  int statusCode = 200;

  Response() {
    print('http post completed');
  }
}

class AuthenticationRepository {
  final secureStorage = SecureStorageService.getInstance;
  String accessToken = '';

  /// Encapsulates the slow init of a http.post call. When all ready, returns
  /// the AuthenticationRepository in a usable state
  Future<AuthenticationRepository> initStorage() async {
    try {
      // Mock http response
      final response = await Future.delayed(Duration(seconds: 2), () => Response());

      if (response.statusCode == 200) {
        accessToken = 'access_token from http value';
        await secureStorage.set(
            key: 'access_token',
            value: accessToken);
        print('access token set');
        // snipped other calls for brevity
      }
    } catch (error, stacktrace) {
      print('Exception occurred: $error stackTrace: $stacktrace');
    }
    return this;
  }
}

class SingleStoragePage extends StatefulWidget {

  @override
  _SingleStoragePageState createState() => _SingleStoragePageState();
}

class _SingleStoragePageState extends State<SingleStoragePage> {
  Future<AuthenticationRepository> authRepo;

  @override
  void initState() {
    super.initState();
    authRepo = AuthenticationRepository().initStorage();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Singleton Storage'),
      ),
      body: Center(
        child: FutureBuilder<AuthenticationRepository>(
          future: authRepo,
          builder: (context, snapshot) {
            print('FutureBuilder re/building');
            if (snapshot.hasData) {
              return Text('Access token: ${snapshot.data.accessToken}');
            }
            return Text('loading...');
          },
        ),
      ),
    );
  }
}

Answered By – Baker

Answer Checked By – Dawn Plyler (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *