Flutter Google Calendar API - clientViaServiceAccount not responding - flutter

I know that there are already answers to this question but I couldn't solve my issue looking at them, so I hope you will be able to help me or at least suggest me what to try because I am not sure what to do or how to debug in order to solve my issue.
I am trying to list all the events in my company calendar using Google Calendar Api.
I used the same code on this question: Using dart and flutter with google calendar api to get a list of events on the a user's calendar
And I followed all the 6 steps of the answer in the question above.
For some reason, the call clientViaServiceAccount do not return any result and any errors.
It seems like it is executing an infinity loop.
This is the code I am using, and I can only see printed "getCalendarEvents". I cannot see the msg "HERE" or any error printed. So The issue is for sure in clientViaServiceAccount.
I edited the code taking into account Iamblichus suggestion, but the issue is that I cannot even reach the CalendarApi. The code is stacked on the clientViaServiceAccount.
import 'package:flutter/material.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:googleapis/calendar/v3.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
CalendarAPI.getCalendarEvents();
return MaterialApp(
title: 'Flutter Demo',
home: Container(),
);
}
}
class CalendarAPI {
static final _accountCredentials = new ServiceAccountCredentials.fromJson(r'''
{
"private_key_id": myPrivatekeyId,
"private_key": myPrivateKey,
"client_email": myClientEmail,
"client_id": myClientId,
"type": "service_account"
}
''');
static final _scopes = [CalendarApi.CalendarScope];
static void getCalendarEvents() {
print('getCalendarEvents');
clientViaServiceAccount(_accountCredentials, _scopes).then((client) {
print('HERE');
var calendar = new CalendarApi(client);
print(calendar);
// Added iamblichus answer
var calendarListEntry = CalendarListEntry();
calendarListEntry.id = calendarId;
calendar.calendarList.insert(calendarListEntry).then((_) {
print('CALENDAR ADDED');
var calEvents = calendar.events.list(calendarId);
calEvents.then((Events events) {
events.items.forEach((Event event) {
print(event.summary);
});
}).catchError((e) => print(e));
}).catchError((e) => print(e));
}).catchError((e) => print(e));
}
}
EDIT FOR ANSWERING iamblichus COMMENT on Jul 15 at 12:31
I created the Service account Credentials on the page of the image shown below.
As soon as I created the key, I file .json was downloaded with the credentials.
I copied the content of that file and paste into ServiceAccountCredentials.fromJson function, so the credentials cannot be wrong.
And even if the credentials were wrong, why cannot I see an error that clientViaServiceAccount call is failing?
I am catching any error and print them in the screen with the last }).catchError((e) => print(e));.
For some reason the call clientViaServiceAccount is not doing anything and I cannot understand how to find the reason for that.

This method works in the cloud. If you want to use it in the flutter app, you have to get the user authenticated using the following plugin,
extension_google_sign_in_as_googleapis_auth

Related

Flutter Hive opens existing box, but is not reading values from it

I am having one of those programming moments where I think I am going mad so hopefully someone can help me.
I have a Flutter app that uses Hive to store data between runs. When the app initially starts, it opens a box and retrieves some information to set the saved theme for the MaterialApp. It then builds the main page for the app and retrieves a range of other options. This was working perfectly (I have a version of it on my phone that works perfectly), but it has stopped working for some reason.
When the app executes, the initial MyApp states that the Hive box is open, but it has no values in it. This is true for a call to an options class to retrieve the options data. After that call, the box suddenly does have values and I am able to retrieve and print out the keys. When the app then builds the main page, it states that the box is open and it has values and is able to retrieve the options data from the options class. Previously, I have had no problems with the first reading of data to extract the theme. I have posted the relevant sections of code below long with the print output from a run.
I am running the app in web and have also run it on a mobile emulator. It has previously worked fine on both platforms, but is now not working on the web platform. It appears to be working fine on the mobile emulator.
The app is using the following versions:
Flutter 2.10.4 • channel stable • https://github.com/flutter/flutter.git
Framework • revision c860cba910 (6 days ago) • 2022-03-25 00:23:12 -0500
Engine • revision 57d3bac3dd
Tools • Dart 2.16.2 • DevTools 2.9.2
pubspec.yaml dependencies:
hive: ^2.0.6
hive_flutter: ^1.1.0
I have upgraded to the latest version of Flutter today to see if that fixed the problem. I had the same issue on the previous stable release.
I have updated to hive 2.1.0 and get the same problem/output.
I have also tried downgrading Flutter to 2.10.0 with Dart 2.16.0, which I know worked fine, and that hasn't solved the problem.
main.dart
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:responsive_sizer/responsive_sizer.dart';
import 'package:lettercloud/data/colours.dart';
import 'package:lettercloud/options/option_page.dart';
const String _boxName = 'lettercloud';
void main() async {
await Hive.initFlutter();
Hive
..registerAdapter(CellAdapter())
..registerAdapter(ThemeModeOptionAdapter());
await Hive.openBox(_boxName);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
final Box _box = Hive.box(_boxName); // Object for hive data access
final Options _options = Options();
final Colours _colours = Colours();
late bool _firstRun = true; // Flag to only read Hive options on first run
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
print('build() Before first run. Extracting box keys. Attempt 1...');
for (String key in _box.keys) {
print('box keys: $key');
}
if (_firstRun) {
print(
'First run. Hive box is open: ${_box.isOpen} Box has values: ${_box.isNotEmpty}');
_options.setHiveBox(_box); // Pass hive object and retrieve options
_firstRun = false;
}
print('');
print('build() After first run. Extracting box keys. Attempt 2...');
for (String key in _box.keys) {
print('box keys: $key');
}
return AnimatedBuilder(
animation: _options.getThemeNotifier(),
builder: (context, child) {
return MaterialApp(
title: 'Lettercloud',
theme: FlexThemeData.light(scheme: FlexScheme.jungle),
darkTheme: FlexThemeData.dark(scheme: FlexScheme.jungle),
themeMode: _options.getThemeMode(),
home: ResponsiveSizer(
builder: (context, orientation, screenType) {
return const MyPage(title: 'Lettercloud Anagram Helper');
},
),
);
});
}
}
class MyPage extends StatefulWidget {
const MyPage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyPage> createState() => MyPageState();
}
class MyPageState extends State<MyPage> {
final Options _options = Options();
late final Box _box; // Object for hive data access
late Widget _displayGrid;
#override
void initState() {
super.initState();
print('Doing init MyPageState');
_box = Hive.box(_boxName);
_options.setHiveBox(_box); // Pass hive object and retrieve options
_setGrid(_options.getGridType());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Lettercloud'),
),
resizeToAvoidBottomInset: false, // Overlay on-screen keyboard
body: SafeArea(
child: _displayGrid,
),
);
}
// Set the grid to display based on the grid type option
void _setGrid(GridType type) {
_displayGrid = _options.getGridType() == GridType.square
? GridSquare(box: _box, options: _options, update: updateGrid)
: GridDiamond(box: _box, options: _options, update: updateGrid);
}
// Callback to set the grid type if the option changes
void updateGrid(GridType type) {
setState(() {
_setGrid(type);
});
}
}
options.dart
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:hive_flutter/hive_flutter.dart';
class Options {
bool _lightMode = true; // Use light colours, or dark
static const String _lightModeName = 'lightMode';
bool _showGrid = true; // Show grid around tiles, or not
static const String _showGridName = 'showGrid';
bool _firstEdit =
true; // Flag to show edit on first start, doesn't need saving
bool _editOnStart = false; // Show edit at startup, or not
static const String _editOnStartName = 'editOnStart';
CharType _charType = CharType.mixed; // Type of letters to show
static const String _charTypeName = 'charType';
ThemeModeOption _themeMode = ThemeModeOption()..setMode(ThemeMode.light);
static const String _themeModeName = 'themeMode';
late Box _box; // Hive object
late final double _tabletF; // Reduction factor for tablet displays
late GridType _gridType = GridType.square;
static const String _gridTypeName = 'gridType';
late GridType _savedGridType = _gridType;
static const String _savedGridTypeName = 'savedGridType';
// last page name - used to control text entry on startup
String _lastPage = PageName.main.toString();
static const String _lastPageName = 'lastPageName';
// Flag to show if the grid type has change. Used to prevent 'show on start'
// triggering the text entry box after the grid layout has been changed by the user
bool _backFromOptionsPage = false;
final String _backFromOptionsPageName = 'fromOptions';
///
/// Hive management methods and global options setting
///
void setHiveBox(Box b) {
_box = b; // Pass the hive management object
print(
'Options hive box. Box is open: ${_box.isOpen} Box has values: ${_box.isNotEmpty}.');
// Set screen size factor for web vs tablet
if (kIsWeb) {
_tabletF = 0.4; // Factor components by 0.4 for web
} else {
_tabletF = 0.6; // Factor components by 0.6 for tablets
}
// Retrieve any option data values
if (_box.get(_lightModeName) != null) {
_lightMode = _box.get(_lightModeName);
} else {
print('Cannot find $_lightModeName');
_box.put(_lightModeName, _lightMode);
}
if (_box.get(_showGridName) != null) {
_showGrid = _box.get(_showGridName);
} else {
_box.put(_showGridName, _showGrid);
}
if (_box.get(_editOnStartName) != null) {
_editOnStart = _box.get(_editOnStartName);
} else {
_box.put(_editOnStartName, _editOnStart);
}
if (_box.get(_charTypeName) != null) {
String temp = _box.get(_charTypeName);
_charType = getCharEnum(temp);
} else {
_box.put(_charTypeName, _charType.toString());
}
if (_box.get(_themeModeName) != null) {
_themeMode = _box.get(_themeModeName);
} else {
_box.put(_themeModeName, _themeMode);
}
if (_box.get(_gridTypeName) != null) {
String temp = _box.get(_gridTypeName);
_gridType = getGridEnum(temp);
} else {
_box.put(_gridTypeName, _gridType.toString());
}
if (_box.get(_savedGridTypeName) != null) {
String temp = _box.get(_savedGridTypeName);
_savedGridType = getGridEnum(temp);
} else {
_box.put(_savedGridTypeName, _savedGridType.toString());
}
if (_box.get(_backFromOptionsPageName) != null) {
_box.put(_backFromOptionsPageName, _backFromOptionsPage);
} else {
_box.put(_backFromOptionsPageName, _backFromOptionsPage);
}
// Load last page value or reset if doesn't exit
if (_box.get(_lastPageName) != null) {
_box.put(_lastPageName, _lastPage);
} else {
_box.put(_lastPageName, _lastPage);
}
_box.flush(); // Make sure everything is written to the disk
}
}
Command line output:
flutter run -d chrome --web-renderer html --web-port 5555
Launching lib\main.dart on Chrome in debug mode...
Waiting for connection from debug service on Chrome... 18.6s
This app is linked to the debug service: ws://127.0.0.1:54752/JAXqfQgauf4=/ws
Debug service listening on ws://127.0.0.1:54752/JAXqfQgauf4=/ws
Running with sound null safety
To hot restart changes while running, press "r" or "R".
For a more detailed help message, press "h". To quit, press "q".
An Observatory debugger and profiler on Chrome is available at: http://127.0.0.1:54752/JAXqfQgauf4=
The Flutter DevTools debugger and profiler on Chrome is available at:
http://127.0.0.1:9101?uri=http://127.0.0.1:54752/JAXqfQgauf4=
build() Before first run. Extracting box keys. Attempt 1...
First run. Hive box is open: true Box has values: false
Options hive box. Box is open: true Box has values: false.
Cannot find lightMode
build() After first run. Extracting box keys. Attempt 2...
box keys: charType
box keys: editOnStart
box keys: fromOptions
box keys: gridType
box keys: lastPageName
box keys: lightMode
box keys: savedGridType
box keys: showGrid
box keys: themeMode
Doing init MyPageState
Options hive box. Box is open: true Box has values: true.
Application finished.
Update #1
Since originally posting I have tried deleting the box and re-running the app in case this was caused by a corrupt file. That hasn't made any difference.
I have also tried adding a .then to the openBox() command in case this is yet another async programming issue, but that hasn't made a difference either, i.e.
await Hive.openBox(_boxName).then((value) {
print('value is $value');
runApp(MyApp());
});
Update #2
So, it took me a while to work this out, but I create my box values on the first run if they don't already exist (to address the use case of the first ever run of the app). If I remove all the put statements in the setHiveBox() method then I get the problem consistently. In other words, there are no values in the box until my Options class creates them when the app runs. This suggests that the data is not being saved to the disk by app. I have compared both main.dart and options.dart with last known working versions and can't see any obvious differences. What could stop the application from saving the data to the disk? Just to note, I have tested another app I developed that uses Hive and this continues to work perfectly. That uses the same version of Hive as this app does.
I fixed this by doing a flutter clean on the project, deleting the flutter installation (deleting the install folder from the disk completely), downloading and re-installing flutter and then doing a flutter pub get on the project folder.
I had previously tried a flutter clean and flutter pub get on their own and this didn't fix the problem so maybe something had gone wrong in the flutter folder itself after the last upgrade? Anyway, a clean install of everything has solved the problem.

Firebase Authorization is not checking if the user is logged in properly

in my flutter app I want to check if a user is logged in before or not and based on this navigate them to either HomePage or SignIn page. The code I am using is not working fine, it is not navigating to the SignIn page after I've done registration and deleted the account in Firebase Console. In short, when I delete a user, who registered well before, in Firebase Console the application is keeping to show the HomePage and all the posts the user made. How can I handle it?
"In short, I want to navigate the user to SignIn page after I delete his account in Firebase Console"
Here is the code in my main.dart file:
_check(){
final FirebaseAuth auth = FirebaseAuth.instance;
final User? user = auth.currentUser;
if(user == null){
HiveDB.deleteUser();
return SignInPage();
}
HiveDB.putUser(id: user.uid);
return HomePage();
}
#override
void initState() {
// TODO: implement initState
super.initState();
_check();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: _check(),
routes: {
SignInPage.id:(context) => SignInPage(),
SignUpPage.id:(context) => SignUpPage(),
HomePage.id:(context) => HomePage(),
},
);
}
The _check() function is not working as it's desired...
While Firebase persists the user info in local storage and automatically restores it when the app/page reloads, this requires that it makes an asynchronous call to the server and thus may not be ready by the time your check runs.
Instead of depending on currentUser, you'll want to listen for changes to the authentication state through the authStateChanges stream. Most apps wrap this in a StreamBuilder and then show the correct screen based on the stream state and last event.
This is something that can be resolved by using async . the problem is you are getting response in future but the action is performed before. Please check the following documentation for detailed overview:
https://dart.dev/codelabs/async-await

Getting null argument while routing through firebase push notifications flutter

I am trying to route to a certain screen with some argument when clicked on push notifications. So far it is working fine when app is in foreground or in background but open. But when the app is terminated it is routing to the correct screen but there is some issue with the argument, it is null.
const payload = admin.messaging.MessagingPayload = {
data : {
'type' : 'msg',
'route' : 'chat-screen',
'argument' : sentby,
},
notification : {
title : senderData.user_name,
body: original.message,
image: notificationIcon,
android_channel_id : "Finiso",
channel_id : "Finiso",
clickAction : 'FLUTTER_NOTIFICATION_CLICK',
}
}
This is the code for the notification which I am triggering through cloud functions.
Future<void> backgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
print(message.data.toString());
print(message.notification.title);
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(backgroundHandler);
added this in main.dart
FirebaseMessaging.instance.getInitialMessage().then((message) async {
if (message != null) {
print(message.data);
routeNotification(message);
}
});
listening to messages and on app open trigger.
void routeNotification(RemoteMessage message) {
final routeName = message.data['route'];
if (routeName == ActivityFeed.routeName) {
Navigator.of(context).pushNamed(ActivityFeed.routeName);
} else {
if (message.data['type'] == 'msg') {
Navigator.pushNamed(
context,
message.data['route'],
arguments: message.data['argument'],
);
}
}
}
This is the routing function I used above
Image showing console logs
It is printing the first two lines from main.dart and "null" is the argument I am trying to get.
Can anyone help me on this. I have no idea what's going on.
Thank you in advance.
I have an answer but I'm not positive on the reason why this is happening yet. Let's collaborate on this.
First of all, I think the routing is happening automatically for you. In all other scenarios except the terminal state, you are pushing the route and handling adding the arguments. In the terminal state, the app is ignoring your initialRoute and using the route from your push notification.
Usually when you setup MaterialApp your initialRoute is used:
MaterialRoute(
initialRoute: '/whatever_you_put_here'
...
)
It seems that WidgetsBinding.instance.platformDispatcher.defaultRouteName (source) is being set within the flutter code and this is causing your default route to be this route instead of whatever you are passing to MaterialApp.
Reading the flutter documentation, it looks like this property is only set when someone calls FlutterView.setInitialRoute from Android.
That is the answer to your question.
--
I don't know what is calling FlutterView.setInitialRoute. A search of FlutterFire code seems to show no instances.

Navigating away when a Flutter FutureProvider resolves it's future

I'm basically looking for a way to either start loading data, or navigate to the Login Screen.
The FutureProvider gets it's value from SharedPreferences. The default homescreen is just a logo with a spinner.
If the userID resolves to null, the app should Navigate to the Login Screen, otherwise it should call a method that will start loading data and then on completion navigate to the main page.
Can this be achieved with FutureProvider?
I add it to the page build to ensure the page widget will subscribe to the Provider:
Widget build(BuildContext context) {
userInfo = Provider.of<UserInfo>(context);
print('Building with $userInfo');
return PageWithLoadingIndicator();
....
I added it to didChangeDependencies to react to the change:
#override
void didChangeDependencies() {
print('Deps changed: $userInfo');
super.didChangeDependencies();
// userInfo = Provider.of<UserInfo>(context); // Already building, can't do this.
// print('And now: $userInfo');
if (userInfo == null) return;
if (userInfo.userId != null) {
startUp(); // Run when user is logged in
} else {
tryLogin(); // Navigate to Login
}
}
And since I can't use Provider.of in initState I added a PostFrameCallback
void postFrame(BuildContext context) {
print('PostFrame...');
userInfo = Provider.of<UserInfo>(context);
}
Main is very simple - it just sets up the MultiProvider with a single FutureProvider for now.
class MyApp extends StatelessWidget {
Future<UserInfo> getUserInfo() async {
String token = await UserPrefs.token;
return UserInfo.fromToken(
token,
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App One',
theme: ThemeData(
primarySwatch: Colors.purple,
),
home: MultiProvider(
providers: [FutureProvider<UserInfo>(builder: (_) => getUserInfo())],
child: LoadingScreen(),
),
);
}
}
The problem is that as it is now I can see from the print statements that "didChangeDependencies" gets called twice, but the value of userInfo is always null, even though build() eventually gets an instance of UserInfo as evident by the print statement in the build() method.
I'm guessing I can add some logic into the build method but that screams against my sensibilities as the wrong place to do it... perhaps it isn't as bad as I think?
I've decided that this is conceptually the wrong approach.
In my case I wanted to use a FutureProvider to take the results from an Async function which create a "Config" object using SharedPreferences. The FutureProvider would then allow the rest of the app to access the user's config settings obtained from sharepreferences.
This still feels to me like a valid approach. But there are problems with this from an app flow perspective.
Mainly that the values from the shared preferences includes the logged in user session token and username.
The app starts by showing a Loading screen with a Circular Progress bar. The app then reads the shared preferences and connects online to check that the session is valid. If there is no session, or if it is not valid, the app navigates to the Login "wizzard" which asks username, then on the next page for the password and then on the next page for 2-factor login. After that it navigates to the landing page. If the loading page found a valid session, the login wizzard is skipped.
The thing is that the two things - app state and app flow are tangenially different. The app flow can result in changes being store in the app state, but the app state should not affect the app flow, at least not in this way, conceptually.
In practical terms I don't think calling Navigator.push() from a FutureProvider's build function is valid, even if context is available. I could be wrong about this, but I felt the flowing approach is more Flutteronic.
#override
void initState() {
super.initState();
_loadSharedPrefs().then((_) {
if(this.session.isValid()) _navToLandingPage();
else _navToLoginStepOne();
}
}
I'm open to better suggestions / guidance

Supporting multiple languages for constant strings in Flutter

I would like to start putting all my constant strings (like labels etc.) into a place that can be translated at a later stage.
How is this handled in Flutter?
Create a Localizations.dart file
Add the following code to that file:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show SynchronousFuture;
class DemoLocalizations {
DemoLocalizations(this.locale);
final Locale locale;
static DemoLocalizations of(BuildContext context) {
return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
}
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'title': 'App title',
'googleLogin': 'Login with Google'
},
'es': {
'title': 'Título de App',
'googleLogin': 'Conectar con Google'
},
};
String get title {
return _localizedValues[locale.languageCode]['title'];
}
String get googleLogin {
return _localizedValues[locale.languageCode]['googleLogin'];
}
}
class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
const DemoLocalizationsDelegate();
#override
bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode);
#override
Future<DemoLocalizations> load(Locale locale) {
// Returning a SynchronousFuture here because an async "load" operation
// isn't needed to produce an instance of DemoLocalizations.
return new SynchronousFuture<DemoLocalizations>(new DemoLocalizations(locale));
}
#override
bool shouldReload(DemoLocalizationsDelegate old) => false;
}
Import Localizations.dart into the file where you use the strings.
Add the delegate DemoLocalizationsDelegate in the MaterialApp
MaterialApp(
localizationsDelegates: [
MyLocalizationsDelegate(),
],
...
)
Substitute new Text("App Title"), with new Text(DemoLocalizations.of(context).title),
For each new string you want to localize, you need to add the translated text to each language's map and then add the String get... line.
It's a bit cumbersome but it does what you need it to.
This is a quick overview of one way of doing it.
You can read more about it in the Flutter docs: https://flutter.io/tutorials/internationalization/
I asked on gitter and I got the following:
Translation/Internationalization isn't a feature we consider "done"
yet. https://pub.dartlang.org/packages/intl works in Flutter. We have
a bug tracking this more generally: flutter/flutter#393
More complete internationalization (i18n) and accessibility support
are two of the big arcs of work ahead of Flutter in the coming months.
Another example of i18n work we have planned, is completing
Right-to-left (RTL) layouts for our provided Widgets (e.g. teaching
the Material library's Scaffold to place the Drawer on the left when
the locale is an RTL language). RTL Text support works today, but
there are no widgets which are out-of-the-box RTL-layout aware at this
moment.