Managing a state in the background via Workmanager

59 Views Asked by At

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?

0

There are 0 best solutions below