Flutter workmanager with BLOC and repository pattern - flutter

I use bloc and with repository pattern. There was a need to execute the code in the background according to the schedule, for this I use workmanager The problem is that the workmanager is executed in a separate method, which is initialized outside the main method, and it does not know anything about the context or the code below like MyApp
#pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((taskName, inputData) {
//How i can access bloc or repository from here?
return Future.value(true);
});
}
void main() async {
Workmanager().initialize(callbackDispatcher, isInDebugMode: true);
Workmanager().registerPeriodicTask('uniqueName', 'taskName',
constraints: Constraints(
networkType: NetworkType.connected,
requiresBatteryNotLow: true,
),
frequency: const Duration(minutes: 5));
}
Workmanager is initialized at the very beginning of the main method and before runApp MyApp, it has no context, and my repository and bloc have not even been created yet. I can’t figure out how to access the bloc from the workmanager.

You can pass the instances of repository and bloc as input data when registering the periodic task and then access them in the callbackDispatcher method:
final Repository _repository = Repository();
final Bloc _bloc = Bloc();
void main() async {
Workmanager().initialize(callbackDispatcher, isInDebugMode: true);
Workmanager().registerPeriodicTask('uniqueName', 'taskName',
inputData: {'repository': _repository, 'bloc': _bloc},
constraints: Constraints(
networkType: NetworkType.connected,
requiresBatteryNotLow: true,
),
frequency: const Duration(minutes: 5));
runApp(MyApp());
}
#pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((taskName, inputData) {
final Repository repository = inputData['repository'];
final Bloc bloc = inputData['bloc'];
// Use repository and bloc as needed
return Future.value(true);
});
}
You can also use a singleton pattern for your repository and bloc classes:
class Repository {
static final Repository _singleton = Repository._internal();
factory Repository() {
return _singleton;
}
Repository._internal();
// Repository methods
}
class Bloc {
static final Bloc _singleton = Bloc._internal();
factory Bloc() {
return _singleton;
}
Bloc._internal();
// Bloc methods
}
void main() async {
Repository();
Bloc();
Workmanager().initialize(callbackDispatcher, isInDebugMode: true);
Workmanager().registerPeriodicTask('uniqueName', 'taskName',
constraints: Constraints(
networkType: NetworkType.connected,
requiresBatteryNotLow: true,
),
frequency: const Duration(minutes: 5));
runApp(MyApp());
}
#pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((taskName, inputData) {
final Repository repository = Repository();
final Bloc bloc = Bloc();
// Use repository and bloc as needed
return Future.value(true);
});
}

Related

How to create a simple bloc test?

I want to write a bloc test. I have a bloc that calls the provider. The provider accesses the repository, where I take a list of groups from the database, convert and return. I don't understand how to write a bloc test for this. How can I implement this?
My Bloc:
class GroupBloc extends Bloc<GroupEvent, GroupState> {
GroupBloc() : super(const NewGroupInitial()) {
on<GetAllGroupsEvent>(
(event, emit) async => emit(
await _getAllGroups(),
),
);
}
final _provider = ProviderInjector.instance.groupProvider;
Future<GroupState> _getAllGroups() async =>
_provider.getGroupList().then(GetAllGroupsState.new);
}
My Provider:
class GroupProvider implements BaseProvider<GroupRepository> {
#override
GroupRepository get repository => injector<GroupRepository>();
Future<List<Group>> getGroupList() => repository.getGroupList();
}
My Repository:
class GroupRepository {
GroupDao dao = GroupDao();
static BaseConverter<GroupEntity, Group> converter = GroupDbConverter();
Future<List<Group>> getGroupList() async {
final getGroupsList = await dao.getAll();
final groupsList = converter.listInToOut(getGroupsList);
return groupsList;
}
}

Flutter RepositoryProvider and Hive LateInitializationError

I have app where I am using Bloc and Hive.
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
Hive.init(appDocumentDirectory.path);
runApp(
const MyApp(),
);
}
On MyApp widget registered MultiRepositoryProvider
return MultiRepositoryProvider(
providers: [
RepositoryProvider(create: (context) => AccountService()),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AccountBloc>(
create: (context) => AccountBloc(context.read<AccountService>()),
),
],
child: MaterialApp(
home: const AppPage(),
),
),
);
AppPage Contains bottomNavigationBar and some pages
account.dart
class AccountService {
late Box<Account> _accounts;
AccountService() {
init();
}
Future<void> init() async {
Hive.registerAdapter(AccountAdapter());
_accounts = await Hive.openBox<Account>('accounts');
}
On appPage have BlocBuilder
BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state.accountStatus == AccountStatus.loading) {
return const CircularProgressIndicator();
} else if (state.accountStatus == AccountStatus.error) {
Future.delayed(Duration.zero, () {
errorDialog(context, state.error);
});
}
return SingleChildScrollView(....
When app first loaded I receive LateInitializationError that late Box <Account> _accounts from account Repository not initialized. But as soon as I navigate to another page and go back, the Box <Account> _accounts are initialized and the data appears.
How can I avoid this error and initialize the Hive box on application load?
Can you try this? I think you need to await Hive init function
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appDocumentDirectory =
await path_provider.getApplicationDocumentsDirectory();
await Hive.init(appDocumentDirectory.path);
runApp(
const MyApp(),
);
}
It's been like 7 months, but if you are still looking for an answer, not sure if it's optimal but below should work.
My understanding on the issue you are having is that the reason why there is that "LateInitializationError" is because that your init function call in your constructor is asynchronously invoked without await for its result. As a result, there is a possibility that when you are calling functions on the box, the initialisation is not yet finished. When you navigate to another page and go back, the function init run happened to be finished. Hence, the error is gone. The complexity here is that constructor can not be marked as async for you to use that await keyword. Since you are using bloc, one possible workaround is to call the init function of your repo when bloc is in init state.
For demo purpose I defined below bloc states and events,
you can absolutely change them based on your needs.
// bloc states
abstract class AccountState{}
class InitState extends AccountState{}
class LoadedState extends AccountState{
LoadedState(this.accounts);
final List<Account> accounts;
}
class LoadingErrorState extends AccountState{}
//bloc events
abstract class AccountEvent {}
class InitEvent extends AccountEvent {}
... // other events
in your bloc logic you can call the init function from you repo on InitEvent
class AccountBloc extends Bloc<AccountEvent, AccountState> {
AccountBloc(this.repo) : super(InitState()) {
on<InitEvent>((event, emit) async {
await repo.init();
emit(LoadedState(account: repo.getAccounts()));
});
...// define handlers for other events
}
final AccountRepository repo;
}
in your service class you can remove the init from the constructor like:
class AccountService {
late Box<Account> _accounts;
AccountService();
Future<void> init() async {
Hive.registerAdapter(AccountAdapter());
_accounts = await Hive.openBox<Account>('accounts');
}
List<Account> getAccounts(){
return _accounts.values.toList();
}
}
Then in your bloc builder, you can add init event to your bloc when the state is InitState as below:
BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state is InitState) {
context.read<AccountBloc>.add(InitEvent());
return const CircularProgressIndicator();
} else if (state is LoadingErrorState) {
Future.delayed(Duration.zero, () {
errorDialog(context, state.error);
});
}
else if (state is LoadedState){
return SingleChildScrollView(....
}
Also, FYI, you can if you want the init to be called when the object of your account service is instantiated, you can take a look at below answer:
https://stackoverflow.com/a/59304510/16584569
However, you still going to need to await for the initialisation of your service. One possible way is just do it in your main function and pass down to your app, but it makes the structure of your code messy and when you want to swap to another repo, you need to remember to change code in main function as well.

Use ObjectBox / Instantiate store and box with Flutter 2.5 and Riverpod

I am using ObjectBox on Flutter 2.5 with Riverpod. The goal is to allow fast caching of a lot of data from an API.
If there is a difference between remote data and that in the box, then we update. The person who no longer has the internet still has good data.
Has anyone managed to instantiate and best use Objectbox in flutter clean architecture with riverpod ? I found several relevant questions about it here and here. but I can't seem to get anything satisfactory at the moment ...
If you have any suggestions for doing this.
I took into account the update that Objectbox made on their example of store creation.
The errors are always the same, either the _box or store is not instantiated or else, get null. I would update my post as I research.
[ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: LateInitializationError: Field '_box#59089156' has not been initialized.
[ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: LateInitializationError: Field 'store' has not been initialized.
------EDIT 21 oct.2021-----
Ok, thanks to the help and comments from my colleagues. I update my code to match. No more store or box instantiation problems.
My code
abstract class LocalDataSourceRepository<T> {
Future<Coin?> getCoinById(dynamic id);
Future<void> addCoin(T object);
}
The remote / local management is in this implementation, with getRemoteDataCoin which retrieves a full currency from the API and transforms it into an entity
class CoinRepositoryImpl extends LocalDataSourceRepository {
final CoinRemoteApi _remoteApi;
final ObjectBoxDatabase _objectBoxDatabase;
CoinRepositoryImpl(
this._remoteApi,
this._objectBoxDatabase,
);
#override
Future<Coin?> getCoinById(id) async {
final _box = _objectBoxDatabase.store.box<Coin>();
final Query<Coin> query = (_boxCoin.query(Coin_.coinId.equals(id))).build();
if (query.findUnique() == null) {
final coin = await getRemoteDataCoin(id);
addCoin(coin);
return query.findUnique();
} else {
return query.findUnique();
}
}
#override
Future<void> addCoin(object) async {
final _box = _objectBoxDatabase.store.box<Coin>();
_box.put(object);
}
Future<Coin> getRemoteDataCoin(selectedCoin) async {
return _remoteApi.getCoinById(
selectedCoin,
const CoinRequest(
localization: 'false',
tickers: false,
marketData: false,
communityData: true,
developerData: false,
sparkline: false
)
).then(
(value) => value.toEntity(),
);
}
}
class ObjectBoxDatabase {
late final Store store;
late final Box<Coin> _box;
ObjectBoxDatabase._create(this.store) {
_box = Box<Coin>(store);
}
static Future<ObjectBoxDatabase> create() async {
final store = await openStore();
return ObjectBoxDatabase._create(store);
}
}
My Riverpod providers.
final localCoinDatabaseProvider = Provider<ObjectBoxDatabase>((ref) => throw UnimplementedError());
final remoteCoinApiProvider = Provider<CoinRemoteApi>((ref) => CoinRemoteApi());
final coinRepositoryProvider = Provider<LocalDataSourceRepository>((ref) => CoinRepositoryImpl(
ref.read(remoteCoinApiProvider),
ref.read(localCoinDatabaseProvider)
));
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final objectBox = await ObjectBoxDatabase.create();
runApp(
ProviderScope(
overrides: [
localCoinDatabaseProvider.overrideWithValue(objectBox)
],
child: const MyApp()
)
);
}
Since you get the ObjectBoxDatabase using the .create method which is a future, you can either use a FutureProvider or overrides
FutureProvider:
final localCoinDatabaseProvider = FutureProvider<ObjectBoxDatabase>((ref) => ObjectBoxDatabase.create());
https://pub.dev/documentation/riverpod/latest/riverpod/FutureProvider-class.html
overrides:
final localCoinDatabaseProvider = Provider<ObjectBoxDatabase>((ref) => throw UnimplementedError());
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final objectBox = await ObjectBoxDatabase.create();
runApp(
const ProviderScope(
overrides: [localCoinDatabaseProvider.overrideWithValue(objectBox)]
child: MyApp()
)
);
}

Can't execute async method in Background with workmanager

In my app, I need to execute some tasks in the background (while app is not running in foreground). I'm able to execute some methods in the background but, I need to execute an async method in the background which I can't.
Here is a part of my code:
void main() {
runApp(MaterialApp(
home: Home(),
));
Workmanager.initialize(callbackDispatcher, isInDebugMode: true);
Workmanager.registerPeriodicTask("1", "simplePeriodicTask",
existingWorkPolicy: ExistingWorkPolicy.replace,
frequency: Duration(minutes: 15),
initialDelay:
Duration(seconds: 5),
constraints: Constraints(
networkType: NetworkType.connected,
));
}
void callbackDispatcher() {
Workmanager.executeTask((task, inputData) {
_HomeState().manager();//This is not Working
print('Background Services are Working!');//This is Working
return Future.value(true);
});
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
void initState() {
login();
super.initState();
}
void manager() async {
if (account == null) {
await login();
await managerLocal();
managerDrive();
} else {
await managerLocal();
managerDrive();
}
}
.......
.......
}
You need to wait for your method to actually finish:
void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
await _HomeState().manager();
print('Background Services are Working!');//This is Working
return true;
});
}
Your manager method should probably return a Future<void>, since it is async.
If you are unsure how to work with Future data, feel free to have a look here.

How to Navigate without context in flutter?

I ended up with using a static function but I need to do navigation and It gave me an error that no getter was found for context so I looked for a solution and found the GET package but when I tried to use it It gave me another error :
E/flutter ( 6078): [ERROR:flutter/lib/ui/ui_dart_state.cc(166)]
Unhandled Exception: NoSuchMethodError: The method 'push' was called on null.
My code:
void main() {
runApp(MyApp());
_MyAppState.autologin();
}
class _MyAppState extends State<MyApp> {
static autologin() async {
var userType;
var store = Firestore.instance;
var auth = FirebaseAuth.instance;
final FirebaseUser user = await auth.currentUser();
store.collection('Users').document(user.uid).get().then((value) {
userType = (value.data)['userType'];
if (userType == 'Student') {
Get.to(StudentsPage());
} else if (userType == 'Teacher') {
} else if (userType == 'Admin') {}
});
}
Create a navigator key
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Assign it to MaterialApp
MaterialApp(
home: Home(),
navigatorKey: navigatorKey
),
Then push your routes by navigatorKey below
navigatorKey.currentState.push(MaterialPageRoute(
builder: (context) => AnotherPage(),
));
or
navigatorKey.currentState.pushNamed(routeName);
This solution is general if you want to navigate or to show Dialog without context using globalKey especially with Bloc or when your logic is separated from your UI part.
Firstly install this package:
Note: I'm using null safety version
get_it: ^7.2.0
Then create a separate file for your service locator:
service_location.dart
import 'package:get_it/get_it.dart';
GetIt locator = GetIt.instance;
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState!.pushNamed(routeName);
}
void setupLocator() {
locator.registerLazySingleton(() => NavigationService());
}
void showMyDialog() {
showDialog(
context: navigatorKey.currentContext!,
builder: (context) => Center(
child: Material(
color: Colors.transparent,
child: Text('Hello'),
),
));
}
}
on main.dart:
void main() {
WidgetsFlutterBinding.ensureInitialized();
NavigationService().setupLocator();
runApp(MyApp());
}
// add navigatorKey for MaterialApp
MaterialApp(
navigatorKey: locator<NavigationService>().navigatorKey,
),
at your business logic file bloc.dart
define this inside the bloc class or at whatever class you want to use navigation inside
Then start to navigate inside any function inside.
class Cubit extends Cubit<CubitState> {
final NavigationService _navigationService = locator<NavigationService>();
void sampleFunction(){
_navigationService.navigateTo('/home_screen'); // to navigate
_navigationService.showMyDialog(); // to show dialog
}
}
Not: I'm using generateRoute for routing.