I have a class that helps me handle the sharedpreferences:
class SharedPref {
static late final SharedPreferences prefs;
static initialize() async {
prefs = await SharedPreferences.getInstance();
}
SharedPref._();
static Future<void> save(String key, String value) async {
await prefs.setString(key, value);
}
// more methods that help me to save/load values...
}
Then I have a testing function:
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
void main() {
testWidgets('Button is present and triggers navigation after tapped',
(WidgetTester tester) async {
final mockObserver = MockNavigatorObserver();
await tester.pumpWidget(
MaterialApp(
home: FirstPage(),
navigatorObservers: [mockObserver],
),
);
expect(find.byType(RaisedButton), findsOneWidget);
await tester.tap(find.byType(RaisedButton));
await tester.pumpAndSettle();
expect(find.byType(MyDetailedPage), findsOneWidget);
});
}
Whenever I run the test:
The following LateError was thrown running a test:
LateInitializationError: Field 'prefs' has not been initialized.
How am I supposed to initialize that? I called the SharedPref.initialize(); before the final mockObserver... but made no change.
Thanks in advance.
I called the SharedPref.initialize(); before
You need to await the call, otherwise you have race conditions when your save is called and initalize may or may not already be finished.
As you mentioned problem was in
final mockObserver = MockNavigatorObserver();
not inside main() function.
But it was another silly mistake with this kind error if wright initialisation like this:
late final MockNavigatorObserver mockObserver;
setUp(() {
mockObserver= MockNavigatorObserver();
})
Problem in this case is using final keyword. If use like that, only one test will pass. You need not using final keyword, instead:
late MockNavigatorObserver mockObserver;
setUp(() {
mockObserver= MockNavigatorObserver();
})
Related
Writing an integration test for an application. An error occurred while running the test:
As I understood from the debugger, my SetUp initializes my variables twice. That is, the variables are initialized, the first test is executed, and then SetUp is initialized again before executing the 2nd test. How can this problem be solved?
My Test:
class MockGroupProvider extends Mock implements GroupProvider {}
class MockStudentProvider extends Mock implements StudentProvider {}
class MockGroupRepository extends Mock implements GroupRepository {}
class MockDatabase extends Mock implements ObjectBox {}
void main() {
final injector = GetIt.instance;
final provider = MockGroupProvider();
final studentProvider = MockStudentProvider();
final repository = MockGroupRepository();
final db = MockDatabase();
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
setUp(() {
injector.registerSingleton<GroupProvider>(provider);
injector.registerSingleton<StudentProvider>(studentProvider);
injector.registerSingleton<GroupRepository>(repository);
injector.registerSingleton<ObjectBox>(db);
});
testWidgets(
"Not inputting a text and wanting to save group display an error: "
"Group name cannot be empty",
(WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: AddGroupPage()));
const IconData iconBtn = Icons.save;
final saveGroupBtn = find.byIcon(iconBtn);
await tester.tap(saveGroupBtn);
await tester.pumpAndSettle();
expect(find.byType(AddGroupPage), findsOneWidget);
expect(find.byType(GroupsPage), findsNothing);
expect(find.text('Group name cannot be empty'), findsOneWidget);
},
);
testWidgets(
"After inputting a text, go to the display page which contains group that same text ",
(WidgetTester tester) async {
await tester.pumpWidget(const MyApp());
const inputText = 'Group 1';
await tester.enterText(
find.byKey(const Key('add_group_field')), inputText);
const IconData iconBtn = Icons.save;
final saveGroupBtn = find.byIcon(iconBtn);
await tester.tap(saveGroupBtn);
await tester.pumpAndSettle();
expect(find.byType(AddGroupPage), findsNothing);
expect(find.byType(GroupsPage), findsOneWidget);
expect(find.text(inputText), findsOneWidget);
},
);
}
The setUp callback runs before each test, therefore it's registering your instances in GetIt multiple times, which will cause an exception to be thrown.
In your case, GetIt should not be necessary here, since no dependency injection appears necessary for your mocks. Instead, you can simply create a new instance of each of your dependencies before each test:
void main() {
late MockGroupProvider provider;
late MockStudentProvider studentProvider;
late MockGroupRepository repository;
late MockDatabase db;
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// Before each test, create new instances for each dependency
setUp(() {
provider = MockGroupProvider();
studentProvider = MockStudentProvider();
repository = MockGroupRepository();
db = MockDatabase();
});
// Tests...
}
I've writen some code that provides a ApiService to a StateNotifier. The ApiService has a dependency on a authenticatorclient - The auth client has to be created asynchronously as it uses sharedprefs to get a token.
Im just trying to figure out if theirs a more elegant way to how I've written this. Basically when the service apiService is injected into the StateNotifier it could be nullable... That to me is a bit of a code smell.
So in brief this is what im doing...
use a FutureProvider to Instantiate the RestClientwith a Dio
authenticatorClient = FutureProvider<RestClient>((ref) async {
final prefs = await SharedPreferences.getInstance();
final dio = Dio();
...
return RestClient(dio);
}
And then I watch that and use a MaybeWhen to return the service
final clientCreatorWatchProvider = Provider<ApiService?>((ref) => ref
.watch(authenticatorClient)
.whenData((value) => ApiService(value))
.maybeWhen(
data: (service) => service,
orElse: () => null,
));
So the bit I dont like is the orElse returning null
And then my StateNotifier is watching...
final AppState = StateNotifierProvider<AppNotifier, String>(
(ref) => AppNotifier(ref.watch(clientCreatorWatchProvider)));
class AppNotifier extends StateNotifier<String> {
final ApiService? apiService;
AppNotifier(this.apiService) : super("loading") {
init();
}
...
}
Any thoughts on the above approach?
Thanks
One way to solve this problem is to initialize SharedPreferences outside of a provider. You can then use ProviderScope to override a synchronous provider, eliminating the need to work with AsyncValue.
When you initialize your app, do the following:
final sharedPreferences = Provider<SharedPreferences>((_) => throw UnimplementedError());
Future<void> main() async {
final sharedPrefs = await SharedPreferences.getInstance();
runApp(
ProviderScope(
overrides: [
sharedPreferences.overrideWithValue(sharedPrefs),
],
child: MyApp(),
),
);
}
Now you could write your providers like so:
final authenticatorClient = Provider<RestClient>((ref) {
final prefs = ref.watch(sharedPreferences);
final dio = Dio();
...
return RestClient(dio);
}
final clientCreatorWatchProvider = Provider<ApiService>((ref) {
final authClient = ref.watch(authenticatorClient);
return ApiService(authClient);
});
I'm trying to create a reactive SharedPreferences utility but I'm stuck with this issue.
This is my class
class SPUtil {
final _workoutsStreamController = StreamController<
Result<Iterable<PreferencesWorkout>, Exception>>.broadcast();
#override
Stream<Result<Iterable<PreferencesWorkout>, Exception>> getWorkouts() async* {
final prefs = await SharedPreferences.getInstance();
_workoutsStreamController.sink.add(success(_getStoredWorkouts(prefs)));
yield* _workoutsStreamController.stream;
}
}
And this is my test
test("getWorkouts SHOULD return empty list WHEN nothing is stored",
() async {
SharedPreferences.setMockInitialValues({});
final actual = await _sut.getWorkouts().first;
expect((actual as Success).value, []);
});
Whenever I run this test it loops for 30 seconds and it returns this error
dart:async _startMicrotaskLoop
TimeoutException after 0:00:30.000000: Test timed out after 30 seconds. See https://pub.dev/packages/test#timeouts
Everything works fine if I use this implementation instead
class SPUtil {
#override
Stream<Result<Iterable<PreferencesWorkout>, Exception>> getWorkouts() async* {
final prefs = await SharedPreferences.getInstance();
yield success(_getStoredWorkouts(prefs));
}
}
So I assume my test is correct.
Thanks in advance.
I finally found the issue, StreamController doesn't emit anything if is not listened, hence the test goes in timeout.
test("getWorkouts SHOULD return empty list WHEN nothing is stored",
() async {
SharedPreferences.setMockInitialValues({});
final actual = _sut.getWorkouts().first;
actual.listen((event) {});
final actualResult = await actual;
expect((actualResult as Success).value, []);
});
When working with HIVE database in flutter. If you ever get error like this:
"Box not found. Did you forget to call Hive.openBox()?"
It means you haven't opened your box to
To resolve this issue call
await Hive.openBox("boxname");
before using the box
It means you haven't opened your box. To resolve this issue call
await Hive.openBox("boxname");
before using the box.
The box needs to be open either at the beginning, after database initialization or right before doing the operation on the box.
For example in my AppDatabase class I have only one box ('book') and I open it up in the initialize() method, like below:
The whole application and tutorial is here.
const String _bookBox = 'book';
#Singleton()
class AppDatabase {
AppDatabase._constructor();
static final AppDatabase _instance = AppDatabase._constructor();
factory AppDatabase() => _instance;
late Box<BookDb> _booksBox;
Future<void> initialize() async {
await Hive.initFlutter();
Hive.registerAdapter<BookDb>(BookDbAdapter());
_booksBox = await Hive.openBox<BookDb>(_bookBox);
}
Future<void> saveBook(Book book) async {
await _booksBox.put(
book.id,
BookDb(
book.id,
book.title,
book.author,
book.publicationDate,
book.about,
book.readAlready,
));
}
Future<void> deleteBook(int id) async {
await _booksBox.delete(id);
}
Future<void> deleteAllBooks() async {
await _booksBox.clear();
}
}
You have to open the box you want to use and make sure to use await while using the openBox() function.
await Hive.openBox("boxname");
I am using Shared Preferences in my Flutter app and what I would like to do is store SharedPreferences as a field on startup and then use it synchronously in the app. However I'm not sure if I'm not missing anything.
What I want to achieve is instead of:
method1() async {
SharedPreferences sp = await SharedPreferences.getInstance();
return sp.getString('someKey');
}
to
SharedPreferences sp;
//I would probably pass SharedPreferences in constructor, but the idea is the same
someInitMethod() async {
sp = await SharedPreferences.getInstance();
}
method1() {
return sp.getString('someKey');
}
method2() {
return sp.getString('someKey2');
}
method3() {
return sp.getString('someKey3');
}
In that way I would achieve synchronous access to sharedPrefs. Is it bad solution?
EDIT:
What is worth mentioning is that getInstance method will only check for instance and if there is any than it returns it, so as I see it, is that async is only needed to initialize instance. And both set and get methods are sync anyway.
static Future<SharedPreferences> getInstance() async {
if (_instance == null) {
final Map<String, Object> fromSystem =
await _kChannel.invokeMethod('getAll');
assert(fromSystem != null);
// Strip the flutter. prefix from the returned preferences.
final Map<String, Object> preferencesMap = <String, Object>{};
for (String key in fromSystem.keys) {
assert(key.startsWith(_prefix));
preferencesMap[key.substring(_prefix.length)] = fromSystem[key];
}
_instance = new SharedPreferences._(preferencesMap);
}
return _instance;
}
I use the same approach as the original poster suggests i.e. I have a global variable (actually a static field in a class that I use for all such variables) which I initialise to the shared preferences something like this:
in globals.dart:
class App {
static SharedPreferences localStorage;
static Future init() async {
localStorage = await SharedPreferences.getInstance();
}
}
in main.dart:
void main() {
start();
}
Async.Future start() async {
await App.init();
localStorage.set('userName','Bob');
print('User name is: ${localStorage.get('userName)'}'); //prints 'Bob'
}
The above worked fine but I found that if I tried to use App.localStorage from another dart file e.g. settings.dart it would not work because App.localStorage was null but I could not understand how it had become null.
Turns out the problem was that the import statement in settings.dart was import 'package:<packagename>/src/globals.dart'; when it should have been import 'globals.dart;.
#iBob101 's answer is good, but still, you have to wait before you use the SharedPreferences for the first time.
The whole point is NOT to await for your SharedPreferences and be sure that it will always be NOT NULL.
Since you'll have to wait anyway let's do it in the main() method:
class App {
static SharedPreferences localStorage;
static Future init() async {
localStorage = await SharedPreferences.getInstance();
}
}
And the main method:
void main() async{
await SharedPref.initSharedPref();
runApp(MyApp());
}
the line await SharedPref.initSharedPref(); takes ~100ms to execute. This is the only drawback as far as I can see.
But you definitely know that in every place in the app your sharedPreferenes instance in NOT NULL and ready for accessing it:
String s = App.localStorage.getString(PREF_MY_STRING_VALUE);
I think it's worthwhile
The cleanest way is to retrieve SharedPreferences in main method and pass it to MyApp as a dependency:
void main() async {
// Takes ~50ms to get in iOS Simulator.
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
runApp(MyApp(sharedPreferences: sharedPreferences));
}
class MyApp extends StatefulWidget {
final SharedPreferences sharedPreferences;
const MyApp({Key key, this.sharedPreferences})
: assert(sharedPreferences != null),
super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
// You can access shared preferences via widget.sharedPreferences
return ...
}
I made a simple way to using this PrefUtil class:
import 'package:shared_preferences/shared_preferences.dart';
class PrefUtil {
static late final SharedPreferences preferences;
static bool _init = false;
static Future init() async {
if (_init) return;
preferences = await SharedPreferences.getInstance();
_init = true;
return preferences;
}
static setValue(String key, Object value) {
switch (value.runtimeType) {
case String:
preferences.setString(key, value as String);
break;
case bool:
preferences.setBool(key, value as bool);
break;
case int:
preferences.setInt(key, value as int);
break;
default:
}
}
static Object getValue(String key, Object defaultValue) {
switch (defaultValue.runtimeType) {
case String:
return preferences.getString(key) ?? "";
case bool:
return preferences.getBool(key) ?? false;
case int:
return preferences.getInt(key) ?? 0;
default:
return defaultValue;
}
}
}
In main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
PrefUtil.init();
.....
Save it like:
PrefUtil.setValue("isLogin", true);
Get the value like:
PrefUtil.getValue("isLogin", false) as bool
By this, it will initialize only once and get it where ever you need.
You can use FutureBuilder to render the loading screen while waiting for SharedPreferences to be intialized for the first time in a singleton-like class. After that, you can access it synchronously inside the children.
local_storage.dart
class LocalStorage {
static late final SharedPreferences instance;
static bool _init = false;
static Future init() async {
if (_init) return;
instance = await SharedPreferences.getInstance();
_init = true;
return instance;
}
}
app_page.dart
final Future _storageFuture = LocalStorage.init();
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _storageFuture,
builder: (context, snapshot) {
Widget child;
if (snapshot.connectionState == ConnectionState.done) {
child = MyPage();
} else if (snapshot.hasError) {
child = Text('Error: ${snapshot.error}');
} else {
child = Text('Loading...');
}
return Scaffold(
body: Center(child: child),
);
},
);
}
my_page.dart
return Text(LocalStorage.instance.getString(kUserToken) ?? 'Empty');
call shared prefs on startup of a stateful main app (we call ours a initState() override of a StatefulWidget after super.initState())
after shared prefs inits, set the value to a field on main (ex: String _someKey)
inject this field into any child component
You can the call setState() on _someKey at you leisure and it will persist to children injected with your field