Flutter Firebase analytics tracking app screen change with Navigator 2.0 - flutter

All examples that I've found are using "navigatorObservers" from the MaterialApp constructor
static FirebaseAnalytics analytics = FirebaseAnalytics.instance;
static FirebaseAnalyticsObserver observer =
FirebaseAnalyticsObserver(analytics: analytics);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase Analytics Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
navigatorObservers: <NavigatorObserver>[observer],
home: MyHomePage(
title: 'Firebase Analytics Demo',
analytics: analytics,
observer: observer,
),
);
}
but my app uses MatterialApp.router from the Navigator 2.0 pattern and could not find an equivalent for attaching an navigatorObserver in order to track screen change events for firebase analytics. Any workarounds or suggestions on this?

The MaterialApp.router constructor has required routerDelegate property. This delegate is usually a wrapper of the Navigator widget. This widget has observers property - that is exactly what you are looking for.
Here is an example of the RouterDelegate, which registers both Firebase and Segment observers:
class AppNavigator extends RouterDelegate<void>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<void> {
AppNavigator({
#required Page<void> initialPage,
this.analyticsObserver,
this.segmentObserver
}) : assert(initialPage != null),
navigatorKey = GlobalKey<NavigatorState>() {
_pagesStack = [initialPage];
}
final FirebaseAnalyticsObserver analyticsObserver;
final SegmentObserver segmentObserver;
...
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [..._pagesStack],
observers: [analyticsObserver, segmentObserver],
onPopPage: (route, dynamic result) {
if (!route.didPop(result)) {
return false;
}
for (final page in _pagesStack) {
if (page == route.settings) {
_pagesStack.remove(page);
notifyListeners();
break;
}
}
return true;
},
);
}
}
Note, that under the hood by default the Firebase Analytics module expects your page routes to have a name property set as a part of RouteSettings:
// From FirebaseAnalyticsObserver
void _sendScreenView(PageRoute<dynamic> route) {
final String? screenName = nameExtractor(route.settings);
if (screenName != null) {
analytics.setCurrentScreen(screenName: screenName).catchError(
(Object error) {
final _onError = this._onError;
if (_onError == null) {
debugPrint('$FirebaseAnalyticsObserver: $error');
} else {
_onError(error as PlatformException);
}
},
test: (Object error) => error is PlatformException,
);
}
}
You can override this behavior by providing custom nameExtractor property to the FirebaseAnalyticsObserver constructor.

Related

Change notifier provider is not updating the consumer in main

I am trying to set the theme of my app on the response of login data after getting the role but my theme is not updating as per expectation. this is how my main() looks. my code is showing no error and I tried to debug nothing seems wrong.
Widget build(BuildContext context) {
return ChangeNotifierProvider<ThemeModel>(
create: (_) => ThemeModel(),
child: Consumer<ThemeModel>(
builder: (context, ThemeModel themeNotifier, child) {
return Sizer(builder: (context, orientation, deviceType) {
return MaterialApp(
theme: themeNotifier.theme == 'consultant'
? counsultantApptheme()
: themeNotifier.theme == 'rmo'
? rmoApptheme()
: counsultantApptheme(),
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
initialRoute: startroute.toString(),
routes: routes,
);
});
}));
and this how I am updating after response of login API
if (snapshot.data!.data!.consultantYN == 'Y') {
Provider.of<ThemeModel>(context, listen: false).theme =
'consultant';
} else {
Provider.of<ThemeModel>(context, listen: false).theme = 'rmo';
}
and this is my function where I am setting theme and calling notifyListeners() in class extends by ChangeNotifier
//theme_model.dart
import 'package:flutter/material.dart';
import 'package:nmc/widgets/theme_config/theme_preference.dart';
class ThemeModel extends ChangeNotifier {
late String _theme;
late ThemePreferences _preferences;
String get theme => _theme;
ThemeModel() {
_theme = 'default';
_preferences = ThemePreferences();
getPreferences();
}
//Switching themes in the flutter apps - Flutterant
set theme(String value) {
_theme = value;
_preferences.setTheme(value);
notifyListeners();
}
getPreferences() async {
_theme = await _preferences.getTheme();
notifyListeners();
}
}

How Can I navigate to different screens on startup based on which type of user<Admin, Customer> is logged in by making the use of Streams

In my flutter app there are two kinds of users, Admin and Customer. I wish to implement a functionality which will navigate The Customer to CustomerHomePage() and Admin to AdminHomePage().
I have wrapped my home property of MaterialApp with StreamBuilder which should to listen to any added values to the currentUserStream and alter the UI accordingly :
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
// home: const MyHomePage(title: 'Flutter Demo Home Page'),
home: Scaffold(
body: StreamBuilder(
initialData: null,
stream: FirestoreServices.currentUserStream,
builder: (context, snapshot) {
Widget widget = LogInPage();
if (snapshot.data != null) {
// Go to AdminHomePage if the logged in User is a Admin
print("Logged in Usertype : ${snapshot.data!.userType.toString()}");
if (snapshot.data!.userType == UserType.admin) {
widget = AdminHomePage(caUser: snapshot.data!);
}
// Go to CustomerHomePage if the logged in User is a Customer
else if (snapshot.data?.userType == UserType.customer) {
widget = CustomerHomePage(
caUser: snapshot.data!,
);
}
} else {
widget = LogInPage();
}
return widget;
}),
));
}
}+
the Stream so used in this streamBuilder is is a static property of FirestoreServices Class which is made the following way :
static Stream<CAUser> get currentUserStream async*{
FirebaseAuth.instance.authStateChanges().map(
(event) async* {
yield await FirestoreServices().uidToCAUser(event!.uid);
});
}
According to me the problem that's occuring is the values are either not getting added to the stream or they aren't getting read by the StreamBuilder. The effect of this is that the screen isn't navigationg to any of the HomePages
I tried the code which I just posted above, and I expect there's something wrong with the getter function.
type here

how to navigate without contextless

I get this error when I open the App.
when I am trying to initialize the router from authStore get this error
"You are trying to use contextless navigation without
a GetMaterialApp or Get.key.
If you are testing your app, you can use:
[Get.testMode = true], or if you are running your app on
a physical device or emulator, you must exchange your [MaterialApp]
for a [GetMaterialApp].
"
main.dart
#override
Widget build(BuildContext context) {
return GetMaterialApp(
navigatorKey: Get.key,
smartManagement: SmartManagement.full,
initialBinding: InitialBinding(),
debugShowCheckedModeBanner: false,
title: 'Ponte Delivery',
initialRoute: Routers.home,
getPages: Pages.getPages,
theme: Themes.light,
);
}
class InitialBinding implements Bindings {
#override
void dependencies() {
Get.lazyPut<LocalStorage>(() => LocalStorageImpl());
Get.put<HttpClient>(HttpClientImpl(Dio()), permanent: true);
Get.put<AuthStore>(AuthStore(Get.find<LocalStorage>()), permanent: true);
}
}
AuthStore
#override
void onInit() {
ever(isLogged, fireRoute);
super.onInit();
token = _storage.read(TOKEN);
user = _storage.read(USER, construct: (map) => UserModel.fromMap(map));
if (token == null && user == null) {
isLogged.value = false;
}
}
fireRoute(logged) {
print(logged);
if (!logged) {
Get.offNamed(Routers.initialRoute);
}
}
I am using already GetMaterialApp.
I tried to use debugger and I got error in this Line
Get.offNamed(Routers.initialRoute);

Flutter read riverpod provider in GoRoute builder

My Flutter project is migrating to go_router and I have trouble understanding how to access riverpod providers in either a GoRoute's build method or redirect method, as I need to access the user's data to control the navigation.
I have a top-level redirect that checks if a user is logged in and sends them to a LoginPage if not. All users can access so-called activities in my app, but only admins can edit them. Whether a user is an admin is stored in a riverpod userDataProvider, which always contains a user if the user is logged in. Now if a user attempts to enter the route /:activityId?edit=true, I want to check whether they are allowed to by accessing the userDataProvider. However, I do not see what the clean way of accessing this provider is.
I found somewhere (can't find the thread anymore), that one way is to use ProviderScope.containerOf(context).read(userDataProvider), but I have never seen this before and it seems a bit exotic to me. Is this the way to go?
My GoRoute looks something like this:
GoRoute(
path: RouteName.event.relPath,
builder: (context, state) {
final String? id = state.params['id'];
final bool edit = state.queryParams['edit'] == 'true';
if (state.extra == null) {
// TODO: Fetch data
}
final data = state.extra! as Pair<ActivityData, CachedNetworkImage?>;
if (edit) {
return CreateActivity(
isEdit: true,
data: data.a,
banner: data.b,
);
}
return ActivityPage(
id: id!,
data: data.a,
banner: data.b,
);
},
redirect: (context, state) {
final bool edit = state.queryParams['edit'] == 'true';
if (edit) {
// IMPORTANT: How to access the ref here?
final bool isAdmin =
ref.read(userDataProvider).currentUser.customClaims.admin;
if (isAdmin) {
return state.location; // Includes the queryParam edit
} else {
return state.subloc; // Does not include queryParam
}
} else {
return state.path;
}
},
),
In my current application, I used something similar approach like this :
Provider registration part (providers.dart) :
final routerProvider = Provider<GoRouter>((ref) {
final router = RouterNotifier(ref);
return GoRouter(
debugLogDiagnostics: true,
refreshListenable: router,
redirect: (context, state) {
router._redirectLogic(state);
return null;
},
routes: ref.read(routesProvider));
});
class RouterNotifier extends ChangeNotifier {
final Ref _ref;
RouterNotifier(this._ref) {
_ref.listen<AuthState>(authNotifierProvider, (_, __) => notifyListeners());
}
String? _redirectLogic(GoRouterState state) {
final loginState = _ref.watch(authNotifierProvider);
final areWeLoggingIn = state.location == '/login';
if (loginState.state != AuthenticationState.authenticated) {
return areWeLoggingIn ? null : '/login';
}
if (areWeLoggingIn) return '/welcome';
return null;
}
}
Main app building as router (app.dart):
class App extends ConsumerWidget {
const App({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context, WidgetRef ref) {
final GoRouter router = ref.watch(routerProvider);
return MaterialApp.router(
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
debugShowCheckedModeBanner: false,
title: 'Flutter Auth',
}
}
}
And as entrypoint (main.dart):
Future<void> main() async {
F.appFlavor = Flavor.dev;
WidgetsFlutterBinding.ensureInitialized();
await setup();
runApp(ProviderScope(
observers: [
Observers(),
],
child: const App(),
));
}

Flutter ViewModel old data is repopulating (using Provider)

I'm using multiProvider to manage state in flutter. The main problem that i'm facing is;
Unable to clear the data inside my viewModel after closing a page. When i open the page again, old data is populating from viewModel.
Because of this issue i've created a 'reset' method in my viewModel & calling this 'reset' method before open the page.
Is there any way to remove old values from viewModel :- Please suggest
My code:
Main Page
Adding providers under multi provider
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: ViewModel1()),
ChangeNotifierProvider.value(value: ViewModel2()),
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
),
//Set Home Page after splash
home: HomePage(),
)
);
}
}
This is my Homepage Navigate to FirstPage & reset ViewModel1 data
HomePage()
{
:
:
:
onPressed(){
Navigator.push(
context, MaterialPageRoute(builder: (context) => FirstPage()));
Provider.of<ViewModel1>(context, listen: false).reset();
}
}
This is My Page:
class FirstPage extends StatefulWidget {
#override
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
#override
Widget build(BuildContext context) {
final ViewModel1 _viewModel1 =
Provider.of<ViewModel1>(context, listen: true);
TextFormField(
controller: _fNameController,
onChanged: _viewModel1.setFname,
);
}
}
This is my ViewModel:
class ViewModel1 with ChangeNotifier {
String _fName = '';
String get fName => _fName;
setFname(String fName) {
_fName = fName;
notifyListeners();
setFnameValidation(fName
.trim()
.isNotEmpty ? true : false);
}
//TODO Reset all values
void reset() {
_fName = '';
notifyListeners();
}
}
If you are reusing one model on different pages, without sharing data between pages.. It's better to create and close the model on every page.