I'm working on an app that is meant to check if a user has entered the workplace or not, when they enter the workplace and when they leave the workplace, and then report this information via an employer. The user should also be able to sign in from home if they're working remotely
Right now this is done via my app's homescreen by two tabs.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tidsrapport/constants.dart';
import 'package:tidsrapport/homewidgets/stampinfromhome.dart';
import 'package:tidsrapport/homewidgets/stampinfromoffice.dart';
class HomeScreen extends ConsumerWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return DefaultTabController(
length: 2,
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Constants.appname,
flexibleSpace: Container(
decoration: BoxDecoration(
gradient: LinearGradient(colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary
], begin: Alignment.topCenter, end: Alignment.bottomCenter),
),
),
toolbarHeight: 110,
centerTitle: true,
automaticallyImplyLeading: false,
bottom: const TabBar(
indicatorColor: Colors.white,
tabs: [
Tab(
icon: Icon(Icons.work, color: Colors.white),
),
Tab(
icon: Icon(Icons.home_work, color: Colors.white),
)
],
),
),
body: TabBarView(
children: [
Container(
alignment: Alignment.center,
child: const StampInFromOffice(),
),
Container(
alignment: Alignment.center,
child: const StampInFromHome(),
),
],
),
),
),
);
}
}
This is the widget the user will use to sign in from home
import 'package:flutter/material.dart';
import 'package:tidsrapport/constants.dart';
import 'package:tidsrapport/home/stampin_info.dart';
import 'package:tidsrapport/widgets/home_portraitview.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class StampInFromHome extends ConsumerStatefulWidget {
const StampInFromHome({Key? key}) : super(key: key);
@override
ConsumerState<StampInFromHome> createState() => _StampInFromHomeState();
}
class _StampInFromHomeState extends ConsumerState<StampInFromHome> {
late double screenWidth;
late double screenHeight;
@override
Widget build(BuildContext context) {
screenHeight = MediaQuery.of(context).size.height;
screenWidth = MediaQuery.of(context).size.width;
bool isSignedIn = ref.watch(inOrOutProvider).signedIn;
String buttonText = isSignedIn ? 'Signed In' : 'Signed Out';
return createPortraitView(
_createHomeStampInWidget(isSignedIn, buttonText),
screenHeight,
screenWidth,
);
}
Widget _createHomeStampInWidget(bool isSignedIn, String buttonText) {
return Container(
width: screenWidth / 1.5,
height: screenHeight / 16,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(20),
),
),
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
foregroundColor: Constants.colorBgWhite,
),
onPressed: () {
setState(() {
if (isSignedIn) {
ref.read(inOrOutProvider.notifier).signOut();
} else {
ref.read(inOrOutProvider.notifier).signIn();
}
});
},
child: Text(buttonText),
),
);
}
}
This widget will be used to sign the user in from the office. I want it to be an automatic process, that when the there is a change to the connection status the users signed-in status will be updated as well and rectified if needed. This functionality uses the connectivity-plus package
import 'dart:async';
import 'dart:developer' as developer;
import 'package:dart_ipify/dart_ipify.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tidsrapport/widgets/home_portraitview.dart';
import 'package:tidsrapport/home/stampin_info.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import '../database/database.dart';
class StampInFromOffice extends ConsumerStatefulWidget {
const StampInFromOffice({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
// TODO: implement createState
return _StampInFromOfficeState();
}
}
class _StampInFromOfficeState extends ConsumerState<StampInFromOffice> {
late bool connectedToWifi;
late String information;
late double screenWidth;
late double screenHeight;
ConnectivityResult _connectionStatus = ConnectivityResult.none;
final Connectivity _connectivity = Connectivity();
late StreamSubscription<ConnectivityResult> _connectivitySubscription;
@override
void dispose() {
_connectivitySubscription.cancel();
super.dispose();
}
Future<void> initConnectivity() async {
late ConnectivityResult result;
try {
result = await _connectivity.checkConnectivity();
} on PlatformException catch (e) {
developer.log('Couldn\'t check connection status', error: e);
return;
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) {
return Future.value(null);
}
return _updateConnectionStatus(result);
}
Future<bool> _testGettingWifi() async {
String ipv4 = await Ipify.ipv4();
developer.log(ipv4);
return await DatabaseService().isInOffice(ipv4);
}
Future<void> _updateConnectionStatus(ConnectivityResult result) async {
setState(
() {
_connectionStatus = result;
if (ConnectivityResult.wifi == _connectionStatus) {
developer.log('Connected to wifi');
} else {
developer.log('Not connected to wifi');
}
},
);
}
@override
void initState() {
// TODO: implement initState
super.initState();
initConnectivity();
_connectivitySubscription =
_connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
}
Widget _createLayout() {
Orientation orientation = MediaQuery.of(context).orientation;
return orientation == Orientation.portrait
? createPortraitView(
_createStampInFromOfficeWidget(), screenHeight, screenWidth)
: _landscapeView();
}
Widget _createStampInFromOfficeWidget() {
return FutureBuilder(
future: _testGettingWifi(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return _errorMessage();
} else if (snapshot.hasData) {
final data = snapshot.data as bool;
if (data == true) {
information = 'Signed in';
ref.read(inOrOutProvider.notifier).signIn();
} else {
ref.read(inOrOutProvider.notifier).signOut();
information = 'Signed out';
}
return _stampedInOrOutTile();
}
}
return const CircularProgressIndicator();
},
);
}
Widget _errorMessage() {
return const Column(
children: [
Icon(
Icons.error_outline_outlined,
color: Color.fromARGB(255, 177, 41, 31),
size: 50,
),
Text('Error: Couldn\'t retrieve public IP adress'),
],
);
}
Widget _landscapeView() {
return Row();
}
Widget _stampedInOrOutTile() {
return Container(
width: (screenWidth / 1.5),
height: (screenHeight / 16),
decoration: BoxDecoration(
gradient: LinearGradient(colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary
], begin: Alignment.topCenter, end: Alignment.bottomCenter),
borderRadius: const BorderRadius.all(
Radius.circular(20),
),
),
child: Center(
child: Text(
information,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 14, color: Colors.white),
),
),
);
}
@override
Widget build(BuildContext context) {
screenHeight = (MediaQuery.of(context).size.height);
screenWidth = (MediaQuery.of(context).size.width);
_connectionStatus == ConnectivityResult.wifi
? connectedToWifi = true
: connectedToWifi = false;
/*
if (isSignedIn) {
color = Constants.colorBgWhite;
= Colors.green;
information = 'Signed out';
} else {
color = Colors.green;
= Constants.colorBgWhite;
information = 'Signed in';
}
*/
Widget screenWidget = _createLayout();
return screenWidget;
}
}
The problem with the functionality as it is now, is that that automatically updating the users signed-in status is when the user is on the StampInFromOffice tab. I want this to be an automated process, that the users status is updated depending on connection status no matter which screen of the app the user is on.
The best way to achieve background tasks when I googled it was via the workmanager package. My idea was that every 15 minutes the app will check the connection status of the device, and then update the users signed-in status depending on the result.
This is what I've done so far
// CallbackDispatcher for background-process
// @pragma('vm:entry-point') Mandatory if the App is obfuscated or using Flutter 3.1+
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tidsrapport/home/stampin_info.dart';
import 'package:workmanager/workmanager.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'firebase_options.dart';
import 'package:dart_ipify/dart_ipify.dart';
import '../database/database.dart';
import 'dart:developer' as developer;
ConnectivityResult connectionStatus = ConnectivityResult.none;
final Connectivity connectivity = Connectivity();
Future<void> initConnection() async {
late ConnectivityResult result;
try {
result = await connectivity.checkConnectivity();
} on PlatformException catch (e) {
developer.log('Couldn\'t check connectivity status', error: e);
return;
}
return determineConnectionStatus(result);
}
Future<void> determineConnectionStatus(ConnectivityResult result) async {
connectionStatus = result;
}
Future<bool> checkIPV4() async {
if (connectionStatus == ConnectivityResult.wifi) {
String ipv4 = await Ipify.ipv4();
developer.log(ipv4);
return await DatabaseService().isInOffice(ipv4);
} else {
return false;
}
}
@pragma('vm:entry-point')
callbackDispatcher(token) {
Workmanager().executeTask(
(task, inputData) async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
int? totalExecutions;
final sharedPreference =
await SharedPreferences.getInstance(); //Initialize dependency
initConnection();
// TODO: Check fro wifi and is at work
// if(provider.hasWifi())
// if(inOffice())
User? user = FirebaseAuth.instance.currentUser;
final idTokenResult = await user?.getIdTokenResult(false);
//print("Test: $idTokenResult");
try {
//add code execution
totalExecutions = sharedPreference.getInt("totalExecutions");
sharedPreference.setInt("totalExecutions",
totalExecutions == null ? 1 : totalExecutions + 1);
developer.log("Total exec: $totalExecutions");
// This works, commented it out for now so it does not keep updating
//await DatabaseService().updateUserLastOnline(DateTime.now());
} catch (err) {
// Logger flutter package, prints error on the debug console
developer.log("Someerror");
developer.log(err.toString());
throw Exception(err);
}
return Future.value(true);
},
);
}
It's similair to what I've done in the StampInFromOffice widget, except I'm not using a StreamSubscription to check for changes in the connection status. The problem I ran into was that I use a NotifierProvider to manage the state of the users signed-in status between the StampInFromOffice and StampInFromHome tabs. I also use that provider to keep track of when the user signs in and when he signs out. I cannot use a provider in the callbackDispatcher because there is no ref I can use to access the provider.
I believe I cannot use UncontrolledProviderScope since my app uses an EvenController as a part of the calender view package.
import 'package:tidsrapport/shared/AppSharedPrefs.dart';
import 'backgroundtask.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:tidsrapport/firebase_options.dart';
import 'package:tidsrapport/route/go_router_provider.dart';
import 'package:tidsrapport/theme/theme.dart';
import 'package:tidsrapport/theme/theme_provider.dart';
import 'package:workmanager/workmanager.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'local_notification_service.dart';
void main() async {
// Required for firebase
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
// Initialize shared preferences
await AppSharedPrefs.ensureInitialized();
// Ask user for permission to show notifications
await Permission.notification.isDenied.then((value) {
if (value) {
Permission.notification.request();
}
});
// Init firebase, WidgetsFlutterBinding.ensureInitialized() required first
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
// Setup for running background task(isolate), will be registered after login
await Workmanager().initialize(
// The callbackDispatcher, must be static or a top level function
callbackDispatcher,
// If enabled, will post a notification whenever the task is running
isInDebugMode: true,
);
// Initialize notifications
LocalNotificationService service = LocalNotificationService();
await service.initialize();
// Whenever the task is changed the old one need to be cancelled, otherwise
// the new task will be ignored and the changes won't be active
Workmanager().cancelAll();
// Show the native splash until FlutterNativeSplash.remove() is called
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
runApp(
// RiverPod
ProviderScope(
// Calendar_View
child: CalendarControllerProvider(
controller: EventController(),
child: const MyApp(),
),
),
);
}
class MyApp extends ConsumerStatefulWidget {
const MyApp({super.key});
@override
ConsumerState<MyApp> createState() => _MyApp();
}
class _MyApp extends ConsumerState<MyApp> {
@override
Widget build(BuildContext context) {
// Get setting for theme, themeProvider uses sharedPreferences
bool isDarkMode = ref.watch(themeProvider).isDarkModeEnabled();
// Go-Router
final router = ref.watch(goRouterProvider);
return MaterialApp.router(
debugShowCheckedModeBanner: false,
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
theme: CustomThemes.lightTheme,
darkTheme: CustomThemes.darkTheme,
routeInformationParser: router.routeInformationParser,
routeInformationProvider: router.routeInformationProvider,
routerDelegate: router.routerDelegate,
title: "TimeTracker",
);
}
}
Is there a way I can use my NotifierProvider in the background process, or do I need a different approach to statemanagement?