How do I track Flutter screens in Firebase analytics? - firebase

I have a Flutter app and I'm testing Google Analytics for Firebase on Flutter.
I wanted to see the routes our users (well, me for now) are visiting. I followed the setup steps in firebase_analytics and I checked their example app, too. I enabled debugging for Analytics as described in the Debug View docs
Unfortunately, the only two kinds of screen views (firebase_screen_class) I receive in my Analytics Debug view are Flutter and MainActivity.
I'd expect to see /example-1, /example-2 and /welcome somewhere, but I don't.
This is the app I'm running in Flutter
class App extends StatelessWidget {
final FirebaseAnalytics analytics = FirebaseAnalytics();
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: <String, WidgetBuilder>{
'/example-1': (_) => Example1(),
'/example-2': (_) => Example2(),
'/welcome': (_) => Welcome(),
},
home: Welcome(),
navigatorObservers: [FirebaseAnalyticsObserver(analytics: analytics)],
);
}
}

This exact use-case is in the documentation for Firebase Analytics under the Track Screenviews section.
Manually tracking screens is useful if your app does not use a separate UIViewController or Activity for each screen you may wish to track, such as in a game.
This is exactly the case with Flutter, as Flutter is taking care of the screen updates: most simple Flutter apps run one single FlutterActivity/FlutterAppDelegate and it takes care of rendering different screens on its own, so letting Firebase Analytics automatically track screens will not bring the desired effect.
As far as my past experience goes, the FirebaseAnalyticsObserver was not very helpful, however, I recommend you, too, check their docs again, they do imply that things should "just work". My best guess is that it didn't work well for me because I didn't use RouteSettings on any of my routes *.
In case FirebaseAnalyticsObserver won't work or apply for your app, the next approach worked quite well for me over the past months of development.
You can set the current screen with FirebaseAnalytics at any point, if you call the setCurrentScreen method with the screen name:
import 'package:firebase_analytics/firebase_analytics.dart';
// Somewhere in your widgets...
FirebaseAnalytics().setCurrentScreen(screenName: 'Example1');
As a first attempt I did this in the widget constructor, but that will not work well and miscount the events: if you pop or push routes, all widget constructors in the stack will be called, even though only the top route really qualifies as "the current screen".
To solve this, we need to use the RouteAware class and only set the current screen in case it's the top route: either our route is added to the stack or the previous top route was popped and we arrived onto the route.
RouteAware comes with boilerplate code and we don't want to repeat that boilerplate for all of our screens. Even for small apps, you have tens of different screens, so I created the RouteAwareAnalytics mixin:
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/widgets.dart';
// A Navigator observer that notifies RouteAwares of changes to state of their Route
final routeObserver = RouteObserver<PageRoute>();
mixin RouteAwareAnalytics<T extends StatefulWidget> on State<T>
implements RouteAware {
AnalyticsRoute get route;
#override
void didChangeDependencies() {
routeObserver.subscribe(this, ModalRoute.of(context));
super.didChangeDependencies();
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPop() {}
#override
void didPopNext() {
// Called when the top route has been popped off,
// and the current route shows up.
_setCurrentScreen(route);
}
#override
void didPush() {
// Called when the current route has been pushed.
_setCurrentScreen(route);
}
#override
void didPushNext() {}
Future<void> _setCurrentScreen(AnalyticsRoute analyticsRoute) {
print('Setting current screen to $analyticsRoute');
return FirebaseAnalytics().setCurrentScreen(
screenName: screenName(analyticsRoute),
screenClassOverride: screenClass(analyticsRoute),
);
}
}
I created an enum to track the screens (and functions to turn the enum to screen names). I used the enums to be able to easily track all routes, refactor route names. Using these enums and functions, I can unit test all possible values and enforce consistent naming: no accidental spaces or special characters, no inconsistent capitalization. There could be other, better ways to determine screen class values, but I went with this approach.
enum AnalyticsRoute { example }
String screenClass(AnalyticsRoute route) {
switch (route) {
case AnalyticsRoute.example:
return 'ExampleRoute';
}
throw ArgumentError.notNull('route');
}
String screenName(AnalyticsRoute route) {
switch (route) {
case AnalyticsRoute.example:
return '/example';
}
throw ArgumentError.notNull('route');
}
Next step in the inital setup is to register the routeObserver as a navigatorObserver of your MaterialApp:
MaterialApp(
// ...
navigatorObservers: [
routeObserver,
// FirebaseAnalyticsObserver(analytics: FirebaseAnalytics()),
],
);
Finally, we can add our first example route that's tracked. Add the with RouteAwareAnalytics to your states and override get route.
class ExampleRoute extends StatefulWidget {
#override
_ExampleRouteState createState() => _ExampleRouteState();
}
class _ExampleRouteState extends State<ExampleRoute> with RouteAwareAnalytics{
#override
Widget build(BuildContext context) => Text('Example');
#override
AnalyticsRoute get route => AnalyticsRoute.example;
}
Every time you add a new route, you can do so with little effort: first, add a new enum value, then the Dart compiler will guide you what to add next: add the screen name and class override values in their respective switch-case. Then, find your state that's building your route, add with RouteAwareAnalytics, and add the route getter.
* The reason why I didn't use RouteSettings is that I prefer Simon Lightfoot's approach with the typed arguments instead of the Object arguments the settings provide:
class ExampleRoute extends StatefulWidget {
const ExampleRoute._({#required this.integer, Key key}) : super(key: key);
// All types of members are supported, but I used int as example
final int integer;
static Route<void> route({#required int integer}) =>
MaterialPageRoute(
// I could add the settings here, though, it wouldn't enforce good types
builder: (_) => ExampleRoute._(integer: integer),
);
// ...
}

Add a Navigation Observer
Add Firebase analytics navigation observer to your MatetialApp:
class MyApp extends StatelessWidget {
FirebaseAnalytics analytics = FirebaseAnalytics();
...
MaterialApp(
home: MyAppHome(),
navigatorObservers: [
FirebaseAnalyticsObserver(analytics: analytics), // <-- here
],
);
That's it! Your analytics should appear in the DebugView:
NOTE!
If it's the first time that you are integrating analytics in your app, it will take about a day for your analytics to appear in your dashboard.
See results right away
To see debug results right away, run the above command on your terminal, then check that they appear in the DebugView:
adb shell setprop debug.firebase.analytics.app [your_app_package_name]
Enjoy!

I experienced the issue for some time and was just able to make it work
The issue for me is that I'm not properly passing settings in MaterialPageRoute
return MaterialPageRoute(
settings: RouteSettings(
name: routeName,
),
builder: (_) => viewToShow);
}
I follow the tutorial on FilledStack and was able to figure out my issue after seeing the sample code

If you are seeing "Flutter" in the firebase_screen_class parameter of the screen_view
event, it means you have it configured properly.
You should find the values you are expecting in the firebase_screen parameter, instead of the firebase_screen_class.
It's also worth checking the firebase_previous_screen parameter to see what was the screen that was open before that one.

Related

Flutter - Is it possible to show the number of users online?

I want to show how many people are using my mobile application instantly in my application. For example: "342 people are currently using the application." or "342 people are online right now." I could not find a solution for this.
I store users data with Firebase. So what I want to do is possible by extracting data from the firebase?
You're simplest and most cost effective way, is to create a document, put in a collection for example called .collection(general), when a user logsIn, add 1 to that value, when they logout, subtract 1, and put this in a stream builder.
After success login, run the following function
await FirebaseFirestore.instance
.collection('general')
.doc('onlineCount)
.update({'membersOnline': FieldValue.increment(1)})//this will increase the number by 1.
);
On logout, substract 1.
this is very easy to handle this logic just save the status when users open your app for eg: on homepage and when they kill your app just update that collection to that particular is offline and at the and do query
where(user:online)
and check the number of users you got and simply show that number.
I hope you got this logic.
A little late to the party. But I would personally recommend making use of the App Lifecycle. Meaning:
detached: The application is still hosted on a flutter engine but is detached from any host views.
inactive: The application is in an inactive state and is not receiving user input. For example during a phone call.
paused: The application is not currently visible to the user and running in the background. This is when you press the Home button.
resumed: The application is visible and responding to user input. In this state, the application is in the foreground.
So you will have to create a StatefulWidget and WidgetsBindingObserver:
import 'package:flutter/material.dart';
class LifeCycleManager extends StatefulWidget {
LifeCycleManager({Key key, #required this.child}) : super(key: key);
final Widget child;
#override
_LifeCycleManagerState createState() => _LifeCycleManagerState();
}
class _LifeCycleManagerState extends State<LifeCycleManager> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
print('AppLifecycleState: $state');
}
#override
Widget build(BuildContext context) {
return widget.child;
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
And then just check the states as follows:
AppLifecycleState _appLifecycleState;
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() {
_appLifecycleState = state;
});
if(state == AppLifecycleState.paused) {
print('AppLifecycleState state: Paused audio playback');
//update user file eg. online_status: offline
}
if(state == AppLifecycleState.resumed) {
print('AppLifecycleState state: Resumed audio playback');
//update user file eg. online_status: online
}
print('AppLifecycleState state: $state');
}

accessing data from a static function globally

in the init() of my splash screen page, i am calling the function of the next page in order to load the data from backend, and meanwhile the splash screen will run.
the issue here is it only calls the static function, and that function stores the data locally.
on my other page, i want data globally, so that i can access that data anywhere on that particular page.
highlights of my code is:
splash screen page init code:
void initState() {
super.initState();
FeedScreen.getdata();
}
and my next page, that is FeedScreen page, where i want data globally is:
class FeedScreen extends StatefulWidget {
#override
_FeedScreenState createState() => _FeedScreenState();
static void getdata() async{
CollectionReference collectionReference = FirebaseFirestore.instance
.collection('Feed');
var snapshot = await collectionReference.get();
snapshot.docs.forEach((result){
collectionReference.doc(result.id).collection('myfeed').snapshots().listen((event) {
var latarr,longarr,titlearr,descarr,urlarr;
for(int i=0;i<event.docs.length;i++){
urlarr.add(event.docs[i].data()['imageurl']);
latarr.add(event.docs[i].data()['lat']);
longarr.add(event.docs[i].data()['long']);
titlearr.add(event.docs[i].data()['title']);
descarr.add(event.docs[i].data()['description']);
}
});
});
}
i want to access the value of latarr,longarr,titlearr,descarr,urlarr outside the getdata() function.
Declare your variables latarr,longarr,titlearr,descarr,urlarr outside any class. For instance in your main.dart file before the void main() function. These variables will be considered as global variables and will be accessible anywhere in your app.
The best and clean approach to do this is that you use State Management. With that, you will be able to manage your variables, etc in your code smoothly and you can access those variables anywhere in your program whenever needed. Some popular ones are Provider
, Bloc and GetX.
By using state management you can easily able to Manipulate and access data anywhere in your project.

Flutter check Firebase is initialised or not and what happens if initialised multiple times?

Scenario: I have three screen of an app that I launch based on condition. One screen is buttons with other two screen options.
It works fine on a screen where I am initialising the firebase and doing fetching and all the stuff fine. void startFirebase() async { await Firebase.initializeApp(); } . However on a second screen I am doing initialisation same way but I am encountered with this error:
No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp().
Question: How to check if it is initialised (to check if initialised on first screen and wont reinitialise on second one) and - what will happen if I initialise Firebase on both the screens or if initialised twice?
update based on first provided answer:
I am not sure but calling initilise twice does not show any error. Heres how I'm trying twice:
#override
void initState() {
startFirebase();
try{
startFirebase();
}catch(e){
print(e.toString());
}
super.initState();
}
//another way:
#override
void initState() {
startFirebase();
startFirebase();
/* try{
startFirebase();
}catch(e){
print(e.toString());
}*/
super.initState();
}
No error on run tab and app works fine.
Firebase init will fail with a different message if you attempt to do it a second time.
FirebaseApp name [DEFAULT] already exists
You can check if it's already initialized as described in this other question.
Unless you have specific needs, you should instead consider instead initializing Firebase just once globally for your main app object when it first launches, and don't worry about it again after that.

Hot reload make my app logout Flutter Firebase

I am working on an e-commerce app. I made all its authentication and storing using Firebase. To enter the app I have to make and account and sign in. While signing in, when I make any change in my code and try to save it using CTRL+s or hot reload the app sign out and I get back to the login screen again!!
I don't get the problem at all !! I don't even know which code should I add with my question !!
Verify if you create de initialize var inside build in main, the correct is create outside build like this:
class myApp extends StatelessWidget {
final Future<FirebaseApp> _init = Firebase.initializeApp();
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _init,
...
)
}

Flutter use Stream or Future?

I'm using Firestore database to store a list of objects. To retrieve them I use the Stream provided by the Firestore package, like this:
class FirestoreApi implements Api {
FirestoreApi._();
static final instance = FirestoreApi._();
#override
Stream<List<Job>> getJobList() {
final path = "users/myUserId/jobs";
final reference = Firestore.instance.collection(path);
final snapshots = reference.snapshots();
return snapshots.map((snapshot) => snapshot.documents.map(
(snapshot) => Job(
id: snapshot.data['uid'],
name: snapshot.data['name']
),
).toList());
}
}
It implements an abstract class:
abstract class Api {
Stream<List<Job>> getJobList();
}
In my Repository class I call it like this:
class Repository {
final FirestoreApi _firestoreApi = FirestoreApi.instance;
Stream<List<job>> getJobList() => _firestoreApi.getJobList();
}
Then in my BloC I call the Repository:
class JobBloc {
final _repository = new Repository();
Stream<List<Job>> getJobList() {
try {
return _repository.getJobList();
} catch (e) {
rethrow;
} finally {}
}
}
And finally here is how I use it in my Widget:
Widget _buildBody(BuildContext context) {
final JobBloc _jobBloc = Provider.of<JobBloc>(context);
return StreamBuilder<List<Job>>(
stream: _jobBloc.getJobList(),
builder: (BuildContext context, AsyncSnapshot<List<Job>> snapshot) {
if (snapshot.hasData) {
return RefreshIndicator(
child: JobList(snapshot.data),
onRefresh: () => _jobBloc.refreshJobList(),
);
} else {
if(snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else {
return Center(child: Text("No data"));
}
}
},
);
}
Until here everything works great and my Widget gets updated in real time when something is changed in the Firestore database.
But now I want to go one step further. Lets say that maybe in the future I need to change my api implementation and use a REST api instead of Firestore. I want that my code is prepared for that.
In that case, all the getJobList() methods should return a Future<List<Job>> since the API will not return a Stream (I don't know if that's possible).
I would have another API class like this that now returns Future<List<Job>>:
class RestApi implements Api {
RestApi._();
static final instance = RestApi._();
#override
Future<List<Job>> getJobList() {
//TODO: my rest api implementation
}
}
So the API abstract class would be modified like this:
abstract class Api {
Future<List<Job>> getJobList();
}
Here the updated Repository:
class Repository {
final RestApi _restApi = RestApi.instance;
Future<List<job>> getJobList() => _restApi.getJobList();
}
And finally in my BloC I would sink the list returned by the API in a StreamController like this:
class JobBloc {
final StreamController _jobController = StreamController<List<Job>>.broadcast();
// retrieve data from stream
Stream<List<Job>> get jobList => _jobController.stream;
Future<List<Job>> getJobList() async {
try {
_jobController.sink.add(await _repository.getJobList());
} catch (e) {
rethrow;
} finally {}
}
}
Now the question: I really like that Firestore returns a Stream, it makes my app to be updated in real time. But on the other hand, I would like that my architecture is consistent.
Since I cannot make my REST api to return a Stream, I think the only way possible would be converting the Firebase Stream to a Future but then I would loose the real-time update feature.
Something like this:
class FirestoreApi implements Api {
FirestoreApi._();
static final instance = FirestoreApi._();
#override
Future<List<Job>> getJobList() async {
final path = "users/myUserId/jobs";
final reference = Firestore.instance.collection(path);
final snapshots = reference.snapshots();
Stream<List<Job>> jobs = snapshots.map((snapshot) => snapshot.documents.map(
(snapshot) => Job(
id: snapshot.data['uid'],
name: snapshot.data['name'],
),
).toList());
List<Job> future = await jobs.first;
return future;
}
}
Until now what I've researched is that using the Future will return only one response, so I will lose the real-time functionality.
I would like to know if loosing the real-time feature would be worthy just to make the architecture consistent or if there is a better approach.
Thanks in advance, any ideas or suggestion will be appreciated.
EDIT: Thanks a lot for your comments, I really appreciate them. I actually don't know which one should be marked as accepted answer since all of them have helped me a lot so I decided to give a positive vote to all of you. If anyone doesn't agree with that or this is not the right behaviour in Stackoverflow please let me know
First of all, in my opinion, firebase is not designed to back up a mature project. In the end, you'll end up with a REST api backing up your app. It's true that, you might also end up using both but for different purposes. So i think you should think about firebase as a tool for MVP/proof of concept. I know that Firebase is cool and works well, etc. but the costs are not feasible for a final product.
Now, nobody says that you can't have a REST client implementation that will return a Stream. Check out this Stream.fromFuture(theFuture). You can think of the REST api like a stream that emits only one event (Rx equivalent: Single)
I would also advise to be careful with the real time update feature provided by Firebase, if you transition to a full REST api, you won't be able to do a real time update because REST doesn't work like that. Firebase is using Sockets for communication (if I remember correctly).
I recommended use the Future way, if you take a break and compare the two codes, with the Future way you need to write more, but the architecture is more clean, strong and scalable. In my experience, that's the right way to do good things. Great work
You can also include both methods in the api / repository, and either retrieve a Future or listen to the Stream in the bloc depending on what you want to do. I don't think you need to worry about violating the consistency of REST by also having a method that returns a stream. There is no better way to tap into the real-time functionality of Firestore than to use a stream like you described.
But to just return a Future, you don't have to go through a stream, you can just await a CollectionReference's getDocuments(), something like this:
class FirestoreApi implements Api {
FirestoreApi._();
static final instance = FirestoreApi._();
CollectionReference jobsReference = Firestore.instance.collection("users/myUserId/jobs");
#override
Future<List<Job>> getJobList() async {
QuerySnapshot query = await jobsReference.getDocuments();
List<Job> jobs = query.documents.map((document) => Job(
id: document.data['uid'],
name: document.data['name'],
)).toList();
return jobs;
}
}
It all depends on your app I think. If real time update is an important feature that effects user experience a lot, stick with the Firebase data streams. If real time updates are not a must, you can get data once using Futures. An alternative to Firebase for real time data updates could be GraphQL subscriptions. I would recommend you to check out Hasura for quick implementation of GraphQL API.
It's a good question.
Firestore vs REST API will result in different APIs (Stream vs Future).
Making the code generic won't work here. As you said:
Stream-based APIs will be realtime
Future-based APIs will not
Even the UX would be different.
In the Stream version, you don't need a refresh indicator.
In the Future version, you can reload the data with pull-to-refresh.
I would not recommend to future-proof your code in this case.
If Firestore works well for you, use Streams in all your APIs.
Only if/when you decide to move to a REST API, then you can convert all your APIs (and UX) to use Futures.
Giving up realtime capabilities upfront doesn't seem worth it.

Resources