Flutter - populating syncfusion calendar with data from Firebase - flutter

I am using the syncfusion_flutter_calendar package. My objective is to populate the calendar with data coming from Firestore.
When I try the code below, I am getting an error that I understand, but I do not find where to fix it. Please, can you help? Thank you.
Error : Unhandled Exception: type 'List' is not a subtype of type 'List'
var myQueryResult;
List<Color> _colorCollection = <Color>[];
MeetingDataSource? events;
final databaseReference = FirebaseFirestore.instance;
class CalendarLastTest extends StatefulWidget {
const CalendarLastTest({Key? key}) : super(key: key);
#override
State<CalendarLastTest> createState() => _CalendarLastTestState();
}
class _CalendarLastTestState extends State<CalendarLastTest> {
#override
void initState() {
_initializeEventColor();
getDataFromFireStore().then((results) {
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {});
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('TEST AGENDA'),
),
body: SfCalendar(
view: CalendarView.month,
initialDisplayDate: DateTime.now(),
dataSource: events,
monthViewSettings: const MonthViewSettings(
appointmentDisplayMode: MonthAppointmentDisplayMode.indicator,
showAgenda: true),
),
);
}
Future<void> getDataFromFireStore() async {
var snapShotsValue = await myQuery();
final Random random = Random();
List<Meeting> list = snapShotsValue.docs
.map((e) => Meeting(
title: e.data()['name'],
description: e.data()['notes'],
from: DateFormat('yyyy-MM-dd HH:mm').parse(e.data()['start_Date']),
to: DateFormat('yyyy-MM-dd HH:mm').parse(e.data()['due_Date']),
backgroundColor: _colorCollection[random.nextInt(9)],
isAllDay: false))
.toList();
setState(() {
events = MeetingDataSource(list);
print (events);
});
}
Future myQuery () async {
// final provider = Provider.of<MeetingProvider>(context, listen: false);
//final provider = Provider.of<MeetingProvider> (context);
final uid = FirebaseAuth.instance.currentUser!.uid;
final path = 'Users/$uid/allTasks';
final currentQuery = FirebaseFirestore.instance.collection(path);
myQueryResult = currentQuery.where('done', isEqualTo : 'No');
myQueryResult =
myQueryResult.where('start_Date', isNotEqualTo: '');
// myQueryResult = myQueryResult.where('due_Date'.length, isEqualTo : 16);
final snapshot = await myQueryResult.get();
return snapshot;
}
void _initializeEventColor() {
_colorCollection = <Color>[];
_colorCollection.add(const Color(0xFF0F8644));
_colorCollection.add(const Color(0xFF8B1FA9));
_colorCollection.add(const Color(0xFFD20100));
_colorCollection.add(const Color(0xFFFC571D));
_colorCollection.add(const Color(0xFF36B37B));
_colorCollection.add(const Color(0xFF01A1EF));
_colorCollection.add(const Color(0xFF3D4FB5));
_colorCollection.add(const Color(0xFFE47C73));
_colorCollection.add(const Color(0xFF636363));
_colorCollection.add(const Color(0xFF0A8043));
}
}

The issue is that the children's type is ListMeeting> the map method did not return that information, resulting in the type exception. You must specify the type of argument (Meeting) to the map method in order to fix this error. Please see the code snippets below.
Future<void> getDataFromFireStore() async
{
var snapShotsValue = await myQuery();
final Random random = Random();
List<Meeting> list = snapShotsValue.docs
.map<Meeting>((e) => Meeting(
eventName: e.data()['name'],
// description: e.data()['notes'],
from: DateFormat('yyyy-MM-dd HH:mm').parse(e.data()['start_Date']),
to: DateFormat('yyyy-MM-dd HH:mm').parse(e.data()['due_Date']),
background: _colorCollection[random.nextInt(9)],
isAllDay: false))
.toList();
setState(() {
events = MeetingDataSource(list);
});
}

Future<void> getDataFromFireStore() async {
// get appointments
var snapShotsValue = await fireStoreReference
.collection("ToDoList")
.where('CalendarType', isNotEqualTo: 'personal')
.get();
// map meetings
List<Meeting> list = snapShotsValue.docs
.map((e) => Meeting(
eventName: e.data()['Subject'],
from: convertTimeStamp(e.data()['StartTime']), //write your own ()
to: convertTimeStamp(e.data()['EndTime']),
background: colorConvert(e.data()['color']), //write your own ()
isAllDay: e.data()['isAllDay'],
recurrenceRule: e.data()['RRULE'],
recurrenceId: e.id,
resourceIds: List.from(e.data()['resourceIds']),
notes: e.data()['notes'],
address: e.data()['Address'].toString(),
geolocation: e.data()['Location'],
calendarType: e.data()['CalendarType'],
id: e.reference,
key: e.id))
.toList();
//get staff then add all to MeetingDataSource
var snapShotsValue2 = await fireStoreReference
.collection("Users")
.where('isStaff', isEqualTo: true)
.get();
List<CalendarResource> resources = snapShotsValue2.docs
.map((e) => CalendarResource(
displayName: e.data()['display_name'],
id: e.reference,
image: NetworkImage(valueOrDefault<String>(
e.data()['photo_url'],
'https',
)),
))
.toList();
setState(() {
events = MeetingDataSource(list, resources);
_employeeCollection = resources;
});
}

Related

I'm making a note app and the NotesView clears all other notes and replaces it with my most recent note

This is the NotesView:
import 'package:app_two/lib/constants/routes.dart';
import 'package:app_two/lib/enums/menu_action.dart';
import 'package:app_two/lib/services/auth/auth_service.dart';
import 'package:app_two/lib/services/auth/crud/notes_service.dart';
import 'package:flutter/material.dart';
class NotesView extends StatefulWidget {
const NotesView({super.key});
#override
State<NotesView> createState() => _NotesViewState();
}
class _NotesViewState extends State<NotesView> {
late Future _myFuture;
late final NotesService _notesService;
String get userEmail => AuthService.firebase().currentUser!.email!;
#override
void initState() {
super.initState();
_notesService = NotesService();
_myFuture = getGet();
}
// #override //delete
// void dispose() {
// _notesService.close();
// super.dispose();
// }
getGet() async {
return await _notesService.getOrCreateUser(email: userEmail);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Your Notes'),
actions: [
IconButton(
onPressed: () {
Navigator.of(context).pushNamed(newNoteRoute);
},
icon: const Icon(Icons.add),
),
PopupMenuButton<MenuAction>(
onSelected: (value) async {
switch (value) {
case MenuAction.logout:
final shouldLogout = await showLogOutDialog(context);
if (shouldLogout) {
await AuthService.firebase().logOut();
Navigator.of(context).pushNamedAndRemoveUntil(
loginRoute,
(_) => false,
);
}
}
},
itemBuilder: (context) {
return const [
PopupMenuItem<MenuAction>(
value: MenuAction.logout,
child: Text('Logout'),
),
];
},
)
],
),
body: FutureBuilder(
future: _myFuture,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return StreamBuilder(
stream: _notesService.allNotes,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.active:
if (snapshot.hasData) {
final allNotes = snapshot.data as List<DatabaseNote>;
return ListView.builder(
itemCount: allNotes.length,
itemBuilder: (context, index) {
final note = allNotes[index];
return ListTile(
title: Text(
note.text,
maxLines: 1,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
);
},
);
} else {
return const CircularProgressIndicator();
}
default:
return const CircularProgressIndicator();
}
},
);
default:
return const CircularProgressIndicator();
}
},
),
);
}
Future<bool> showLogOutDialog(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Sign out'),
content: const Text('Are you sure you want to sign out?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: const Text('Cancel')),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: const Text('Logout'))
],
);
},
).then((value) => value ?? false);
}
}
//Notes_service
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' show join;
import 'crud_exceptions.dart';
class NotesService {
Database? _db;
List<DatabaseNote> _notes = [];
static final NotesService _shared = NotesService._sharedInstance();
NotesService._sharedInstance() {
_notesStreamController = StreamController<List<DatabaseNote>>.broadcast(
onListen: () {
_notesStreamController.sink.add(_notes);
//populate stream with a stream of notes that we've already read from the database
},
);
}
factory NotesService() => _shared; //singleton
late final StreamController<List<DatabaseNote>> _notesStreamController;
Stream<List<DatabaseNote>> get allNotes => _notesStreamController.stream;
Future<DatabaseUser?> getOrCreateUser({required String email}) async {
try {
final user = await getUser(email: email);
return user;
} on CouldNotFindUser {
final createdUser = await createUser(email: email);
return createdUser;
} on DatabaseIsNotOpen {
open();
} catch (e) {
print(e.toString());
rethrow;
}
}
Future<void> _cacheNotes() async {
final allNotes = await getAllNotes();
_notes = allNotes.toList();
_notesStreamController.add(_notes);
}
Future<DatabaseNote> updateNote({
required DatabaseNote note,
required String text,
}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
//make sure note exists
await getNote(id: note.id);
//update db
final updatesCount = await db.update(noteTable, {
textColumn: text,
isSyncedwithCloudColumn: 0,
});
if (updatesCount == 0) {
throw CouldNotUpdateNote();
} else {
final updatedNote = await getNote(id: note.id);
_notes.removeWhere((note) => note.id == updatedNote.id);
_notes.add(updatedNote);
_notesStreamController.add(_notes);
return updatedNote;
}
}
Future<Iterable<DatabaseNote>> getAllNotes() async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final notes = await db.query(noteTable);
return notes.map((noteRow) => DatabaseNote.fromRow(noteRow));
}
Future<DatabaseNote> getNote({required int id}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final notes = await db.query(
noteTable,
limit: 1,
where: 'id = ?',
whereArgs: [id],
);
if (notes.isEmpty) {
throw CouldNotFindNote();
} else {
final note = DatabaseNote.fromRow(notes.first);
_notes.removeWhere((note) => note.id == id);
_notesStreamController.add(_notes);
return note;
}
}
Future<int> deleteAllNotes() async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final numberOfdeletions = await db.delete(noteTable);
_notes = [];
_notesStreamController.add(_notes);
return numberOfdeletions;
}
Future<void> deleteNote({required int id}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final deletedCount = await db.delete(
noteTable,
where: 'id = ?',
whereArgs: [id],
);
if (deletedCount == 0) {
throw CouldNotDeleteNote();
} else {
_notes.removeWhere((note) => note.id == id);
_notesStreamController.add(_notes);
}
}
Future<DatabaseNote> createNote({required DatabaseUser owner}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
//Make sure owner exists in database withthe correct id
final dbUser = await getUser(email: owner.email);
if (dbUser != owner) {
throw CouldNotFindUser();
}
const text = '';
//create the note
final noteId = await db.insert(noteTable, {
userIdColumn: owner.id,
textColumn: text,
isSyncedwithCloudColumn: 1,
});
final note = DatabaseNote(
id: noteId,
userId: owner.id,
text: text,
isSyncedWithCloud: true,
);
_notes.add(note);
_notesStreamController.add(_notes);
return note;
}
Future<DatabaseUser> getUser({required String email}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final results = await db.query(
userTable,
limit: 1,
where: 'email = ?',
whereArgs: [email.toLowerCase()],
);
if (results.isEmpty) {
throw CouldNotFindUser();
} else {
return DatabaseUser.fromRow(results.first);
}
}
Future<DatabaseUser> createUser({required String email}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
final results = await db.query(
userTable,
limit: 1,
where: 'email = ?',
whereArgs: [email.toLowerCase()],
);
if (results.isNotEmpty) {
throw UserAlreadyExists();
}
final userId = await db.insert(userTable, {
emailColumn: email.toLowerCase(),
});
return DatabaseUser(id: userId, email: email);
}
Future<void> deleteUser({required String email}) async {
final db = _getDatabaseorThrow();
final deletedCount = await db.delete(
userTable,
where: 'email = ?',
whereArgs: [email.toLowerCase()],
);
if (deletedCount != 1) {
throw CouldNotDeleteUser();
}
}
Database _getDatabaseorThrow() {
final db = _db;
if (db == null) {
throw DatabaseIsNotOpen();
} else {
return db;
}
}
Future<void> close() async {
final db = _db;
if (db == null) {
try {
await open();
} on DatabaseIsNotOpen {
open();
}
// throw DatabaseIsNotOpen();
} else {
await db.close();
_db = null;
}
}
Future<void> _ensureDbIsOpen() async {
if (_db != null) {
try {
await open();
} on DatabaseAlreadyOpenException {
//empty
}
}
}
Future<void> open() async {
if (_db != null) {
throw DatabaseAlreadyOpenException();
}
try {
await _ensureDbIsOpen();
final docsPath = await getApplicationDocumentsDirectory();
final dbPath = join(docsPath.path, dbName);
final db = await openDatabase(dbPath);
_db = db;
//create user table
await db.execute(createUserTable);
//create note table
await db.execute(createNoteTable);
await _cacheNotes();
} on MissingPlatformDirectoryException {
throw UnableToGetDocumentDirectory();
}
}
}
#immutable
class DatabaseUser {
final int id;
final String email;
const DatabaseUser({
required this.id,
required this.email,
});
DatabaseUser.fromRow(Map<String, Object?> map)
: id = map[idColumn] as int,
email = map[emailColumn] as String;
#override
String toString() => 'Person, ID = $id, email= $email';
#override
bool operator ==(covariant DatabaseUser other) => id == other.id;
#override
int get hashCode => id.hashCode;
}
class DatabaseNote {
final int id;
final int userId;
final String text;
final bool isSyncedWithCloud;
DatabaseNote({
required this.id,
required this.userId,
required this.text,
required this.isSyncedWithCloud,
});
DatabaseNote.fromRow(Map<String, Object?> map)
: id = map[idColumn] as int,
userId = map[userIdColumn] as int,
text = map[textColumn] as String,
isSyncedWithCloud =
(map[isSyncedwithCloudColumn] as int) == 1 ? true : false;
#override
String toString() =>
'Note, ID = $id, userId = $userId, isSyncedWithCloud = $isSyncedWithCloud, text = $text';
#override
bool operator ==(covariant DatabaseNote other) => id == other.id;
#override
int get hashCode => id.hashCode;
}
const dbName = 'notes.db';
const noteTable = 'note';
const userTable = 'user';
const idColumn = 'id';
const emailColumn = 'email';
const userIdColumn = 'user_id';
const textColumn = 'text';
const isSyncedwithCloudColumn = 'is_synced_with_cloud';
const createUserTable = '''CREATE TABLE IF NOT EXISTS "user" (
"id" INTEGER NOT NULL,
"email" TEXT NOT NULL UNIQUE,
PRIMARY KEY("id" AUTOINCREMENT)
);''';
const createNoteTable = '''CREATE TABLE IF NOT EXISTS "note" (
"id" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL,
"text" TEXT,
"is_synced_with_cloud" INTEGER DEFAULT 0,
FOREIGN KEY("user_id") REFERENCES "user"("id"),
PRIMARY KEY("id" AUTOINCREMENT)
);''';
I'm making a note app and the NotesView clears all other notes and replaces it with my most recent not. I have no idea why this is happening
Each note you input should display on a tile. But instead this happens:
But only after I restart the app. In the next images, I add a new note and then restart the application
I believe I did something very in my StreamBuilder but don't know what.
I'd appreciate any assistance.
Try changing the case ConnectionState.active to case ConnectionState.done in your StreamBuilder.
Duplication may be occurring due to building your list inside case ConnectionState.active.
I don't know what your db.update does or your SQL query looks like but if you don't pass the id how are you supposed to update the note?
Future<DatabaseNote> updateNote({
required DatabaseNote note,
required String text,
}) async {
await _ensureDbIsOpen();
final db = _getDatabaseorThrow();
//make sure note exists
await getNote(id: note.id);
//update db
final updatesCount = await db.update(noteTable, { ///how does magically know what note to update?
textColumn: text,
isSyncedwithCloudColumn: 0,
});
if (updatesCount == 0) {
throw CouldNotUpdateNote();
} else {
final updatedNote = await getNote(id: note.id);
_notes.removeWhere((note) => note.id == updatedNote.id);
_notes.add(updatedNote);
_notesStreamController.add(_notes);
return updatedNote;
}
}
If you're using INSERT OR REPLACE INTO because you don't pass the id you're creating a new row instead of updating it and your cache list is giving you false information because at the end you think you update it correctly and just replace the note with the same id, your cache list doesn't reflect the real information anymore.
If you're updating all notes in your table because the where clause doesn't have an id you will present your current problem when restarting
Check your db layer apart to see if you're actually doing what you want.

Riverpod - widget stays in loading state when listening to a streamProvider

I can't seem to get my ref.watch(mapProvider).when( to build the data case of my widget:
#override
Widget build(BuildContext context, WidgetRef ref) {
if (defaultTargetPlatform == TargetPlatform.android) {
AndroidGoogleMapsFlutter.useAndroidViewSurface = true;
}
_vm = ref.watch(findPageVm);
_widgetRef = ref;
final isStillLoading = useState(true);
return ref.watch(findStoreProvider).when(
data: (store) {
_store = store;
store.$search();
return Stack(children: [
const VpLoadingPage(
noText: true,
),
AnimatedOpacity(
curve: Curves.fastOutSlowIn,
opacity: isStillLoading.value ? 0 : 1.0,
duration: GOOGLE_MAP_OVERLAY_DURATION,
child: Stack(children: [
ref.watch(mapProvider).when(
data: (snapshot) {
return GoogleMap(
markers: snapshot.markers,
myLocationEnabled: true,
initialCameraPosition: CameraPosition(
target: LatLng(_store!.userPosition.latitude,
_store!.userPosition.longitude),
zoom: snapshot.zoom,
),
onMapCreated: (GoogleMapController controller) {
_vm.controller.complete(controller);
Future.delayed(const Duration(milliseconds: 550),
() => isStillLoading.value = false);
},
);
},
error: (error, stackTrace) => Text(error.toString()),
loading: () {
return const VpLoadingPage(noText: true);
}),
mapOverlay()
]))
]);
},
error: (error, stackTrace) => Text(error.toString()),
loading: () => const VpLoadingPage(noText: true));
}
provider:
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:vepo/src/application/local/map_data/map.dart';
import 'package:vepo/src/domain/vegan_item_establishment/_common/vegan_item_establishment.dart';
import 'package:vepo/src/presentation/pages/find/search_results_provider.dart';
import 'package:vepo/src/presentation/stores/store.dart';
import 'package:vepo/src/presentation/stores/user_store.dart';
final findStoreProvider = FutureProvider((ref) async {
final userStore = await ref.watch(userStoreProvider.future);
final store = FindStore(ref, userStore: userStore);
await store.$search();
return store;
});
final mapProvider = StreamProvider<VpMap>((ref) async* {
final store = await ref.watch(findStoreProvider.future);
store.map$!.listen((event) {
inspect(event);
});
store.mapController$.add(VpMap.empty());
yield* store.map$!;
});
class FindStore extends Store {
factory FindStore(Ref $provider, {required UserStore userStore}) {
instance = FindStore._internal($provider, userStore);
instance.map$ = instance.mapController$.stream;
instance._userStore = userStore;
return instance;
}
FindStore._internal(this.$provider, this._userStore);
static late FindStore instance;
final Ref $provider;
Stream<VpMap>? map$;
VpMap? map;
late final StreamController<VpMap> mapController$ =
StreamController.broadcast();
late UserStore _userStore;
Set<Marker> markers = <Marker>{};
Stream<String?>? searchTerm;
Position get userPosition => _userStore.userPosition;
Future<List<VgnItmEst>> _$search(String searchTerm) async {
final result =
await $provider.watch(searchResultsProvider(searchTerm).future);
return result;
}
Set<Marker> _fromSearchResults(List<VgnItmEst> searchResults) {
return searchResults
.map((searchResult) => Marker(
markerId: MarkerId(searchResult.id.toString()),
position: LatLng(searchResult.establishment!.location!.lat,
searchResult.establishment!.location!.lng)))
.toSet();
}
Future<FindStore> $search([String searchTerm = '']) async {
final searchResults = await _$search(searchTerm);
markers = _fromSearchResults(searchResults);
map = VpMap(position: _userStore.userPosition, markers: markers);
mapController$.add(map!);
return this;
}
}
I've been trying to make FindStore a singleton as I thought it would be due to getting a different FindStore but it did not fix it. I have inspected the values being emitted with store.map$!.listen((event) { and there is one value emitted. Why does the widget remain in the loading state when reading mapProvider?

Refresh page if data isn't shown on screen

I have a Future in my initState function that gets jwt from cache and uses it to get the logged in user's details. The initState function is:
#override
void initState() {
super.initState();
Future.delayed(Duration.zero, () async {
final token = await CacheService().readCache(key: "jwt");
if (token != null) {
await Provider.of<ProfileNotifier>(context, listen: false)
.decodeUserData(
context: context,
token: token,
option: 'home',
);
}
});
}
Now, it does work and I do get the data, but not on the first run. I have to either hot reload the emulator or navigate to another page and come back for the page to rebuild itself and show the data on screen. I don't understand why it doesn't show the data on the first run itself.
ProfileNotifier class:
class ProfileNotifier extends ChangeNotifier {
final ProfileAPI _profileAPI = ProfileAPI();
final CacheService _cacheService = CacheService();
ProfileModel _profile = ProfileModel(
profileImage: "",
profileName: "",
profileBio: "",
);
AccountModel _account = AccountModel(
userId: "",
userEmail: "",
userPassword: "",
);
ProfileModel get profile => _profile;
AccountModel get account => _account;
Future decodeUserData({
required BuildContext context,
required String token,
required String option,
}) async {
try {
_profileAPI.decodeUserData(token: token).then((value) async {
final Map<String, dynamic> parsedData = await jsonDecode(value);
var userData = parsedData['data'];
if (userData != null) {
List<String>? userProfileData = await _cacheService.readProfileCache(
key: userData['userData']['id'],
);
if (userProfileData == null) {
final isProfileAvailable =
await Provider.of<ProfileNotifier>(context, listen: false)
.getProfile(
context: context,
userEmail: userData['userData']['userEmail'],
);
if (isProfileAvailable is ProfileModel) {
_profile = isProfileAvailable;
} else {
_account = AccountModel(
userId: userData['userData']['id'],
userEmail: userData['userData']['userEmail'],
userPassword: userData['userData']['userPassword'],
);
_profile = ProfileModel(
profileImage: '',
profileName: '',
);
}
if (option != 'profileCreation' && isProfileAvailable == false) {
Navigator.of(context).pushReplacementNamed(ProfileCreationRoute);
}
} else {
_account = AccountModel(
userId: userData['userData']['id'],
userEmail: userData['userData']['userEmail'],
userPassword: userData['userData']['userPassword'],
);
_profile = ProfileModel(
profileName: userProfileData[3],
profileImage: userProfileData[4],
profileBio: userProfileData[5],
);
}
} else {
Navigator.of(context).pushReplacementNamed(AuthRoute);
}
notifyListeners();
});
} catch (e) {
debugPrint('account/profileNotifier decode error: ' + e.toString());
}
}
Future getProfile({
required BuildContext context,
required String userEmail,
}) async {
try {
var getProfileData = await _profileAPI.getProfile(
userEmail: userEmail,
);
final Map<String, dynamic> parsedProfileData =
await jsonDecode(getProfileData);
bool isReceived = parsedProfileData["received"];
dynamic profileData = parsedProfileData["data"];
if (isReceived && profileData != 'Fill some info') {
Map<String, dynamic> data = {
'id': (profileData['account']['id']).toString(),
'userEmail': profileData['account']['userEmail'],
'userPassword': profileData['account']['userPassword'],
'profile': {
'profileName': profileData['profileName'],
'profileImage': profileData['profileImage'],
'profileBio': profileData['profileBio'],
}
};
AccountModel accountModel = AccountModel.fromJson(
map: data,
);
return accountModel;
} else {
return false;
}
} catch (e) {
debugPrint('profileNotifier getProfile error: ' + e.toString());
}
}
Future setProfile({
required String profileName,
required String profileImage,
required String profileBio,
}) async {
_profile.profileName = profileName;
_profile.profileImage = profileImage;
_profile.profileBio = profileBio;
await _cacheService.writeProfileCache(
key: _account.userId,
value: [
_account.userId,
_account.userEmail,
_account.userPassword as String,
profileName,
profileImage,
profileBio,
],
);
notifyListeners();
}
}
CacheService class:
class CacheService {
Future<String?> readCache({
required String key,
}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
String? cache = await sharedPreferences.getString(key);
return cache;
}
Future<List<String>?> readProfileCache({
required String key,
}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
List<String>? cachedData = await sharedPreferences.getStringList(key);
return cachedData;
}
Future writeCache({required String key, required String value}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
await sharedPreferences.setString(key, value);
}
Future writeProfileCache(
{required String key, required List<String> value}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
await sharedPreferences.setStringList(key, value);
}
Future deleteCache({
required BuildContext context,
required String key,
}) async {
final SharedPreferences sharedPreferences =
await SharedPreferences.getInstance();
await sharedPreferences.remove(key).whenComplete(() {
Navigator.of(context).pushReplacementNamed(AuthRoute);
});
}
}
I can't seem to figure out the problem here. Please help.
EDIT: The data is used to show profileImage of user in CircleAvatar like this:
#override
Widget build(BuildContext context) {
ProfileModel profile =
Provider.of<ProfileNotifier>(context, listen: false).profile;
return GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: Scaffold(
drawer: const ProfileDrawer(),
appBar: AppBar(
backgroundColor: Colors.white,
leading: Row(children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 9),
child: Builder(builder: (BuildContext context) {
return InkWell(
onTap: () => Scaffold.of(context).openDrawer(),
child: CircleAvatar(
maxRadius: 20.0,
backgroundImage: profile.profileImage.isNotEmpty
? NetworkImage(profile.profileImage)
: null,
child: profile.profileImage.isEmpty
? SvgPicture.asset(
'assets/images/profile-default.svg')
: null),
);
}),
), ....
This CircleAvatar in the appBar shows the image only after the page is rebuilt. There's nothing else on the page except the appbar for now.
When we use ChangeNotifier, it provides two options to access the data. These are:
Read the data - You read the data, it doesn't act as a Stream or State and only one time. This is what you're doing in your case.
Advantage - Whenever the data is needed only one time, for example - Mathematical calculation, you use this.
Disadvantage - It doesn't listen to the changes and the data returned is static.
Watch the data - What you need. It provides the data in a state manner, wherever you access the data using Watch, it (or the widget in the data is used) will be updated whenever the underlying data is updated, even from other Screens/Widgets.
Advantage - The data result is dynamic and the widget is updated whenever the data is updated.
Disadvantage - In case where static data works, it is unnecessary plus it may affect any operations dependent on the data.
There are two ways to use Read and Watch.
The normal functions provided by the Author of the package
//For reading the data
var yourData = Provider.of<YourNotifier>(context, listen: false);
//For watching the data
var yourData = Provider.of<ProfileNotifier>(context, listen: true);
The extension functions on BuildContext provided by the Author:
//For reading the data
var yourData = context.read<YourNotifier>();
//For watching the data
var yourData = context.watch<YourNotifier>();
So, what you need to do is:
Change
ProfileModel profile =
Provider.of<ProfileNotifier>(context, listen: false).profile;
to
ProfileModel profile =
Provider.of<ProfileNotifier>(context, listen: true).profile;
//Or
ProfileModel profile = context.watch<ProfileNotifier>().profile;
Edit: Also, considering good UX, you can use a bool flag to update the UI whenever the data is loaded and if it's loading, show a CircularProgressIndicator.

Is there a way to access data coming from BLE device from single file which keeps updating?

I want to access characteristic values of BLE from one dart file, What I am doing is that I am connecting the device from one activity and then sending the device info to all other activities. But to get values I have to write the same code again and again to all activities/dart files.
For example i am connecting device in an activity like this:
StreamBuilder<List<ScanResult>>(
stream: FlutterBlue.instance.scanResults,
initialData: [],
builder: (c, snapshot) => Column(
children: snapshot.data
.map(
(r) => ScanResultTile(
result: r,
onTap: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
r.device.connect();
print('DEVICE CONNECTED');
return BluetoothConnectedSuccess(device: r.device);
Here device: r.device is the device that i have connected to my Flutter App. Now if i want to display device data i have to initilaze these lines of code everytime i jump to any screen/activity:
class BluetoothConnectedSuccess extends StatefulWidget {
const BluetoothConnectedSuccess({Key key, this.device}) : super(key: key);
final BluetoothDevice device;
#override
_BluetoothConnectedSuccessState createState() =>
_BluetoothConnectedSuccessState();
}
class _BluetoothConnectedSuccessState extends State<BluetoothConnectedSuccess> {
// BLE
final String SERVICE_UUID = "4fafc201-1fb5-459e-8fcc-c5c9c331914b";
final String CHARACTERISTIC_UUID = "beb5483e-36e1-4688-b7f5-ea07361b26a8";
bool isReady;
Stream<List<int>> stream;
List<int> lastValue;
List<double> traceDust = List();
#override
void initState() {
super.initState();
isReady = false;
connectToDevice();
}
connectToDevice() async {
await widget.device.connect();
discoverServices();
}
discoverServices() async {
List<BluetoothService> services = await widget.device.discoverServices();
services.forEach((service) {
if (service.uuid.toString() == SERVICE_UUID) {
service.characteristics.forEach((characteristic) {
if (characteristic.uuid.toString() == CHARACTERISTIC_UUID) {
characteristic.setNotifyValue(!characteristic.isNotifying);
stream = characteristic.value;
print(stream);
lastValue = characteristic.lastValue;
print(lastValue);
setState(() {
isReady = true;
});
}
});
}
});
}
_dataParser(List<int> data) {
var value = Uint8List.fromList(data);
print("stream.value: $value"); // stream.value: [33]
var hr = ByteData.sublistView(value, 0, 1);
print("Heart rate: ${hr.getUint8(0)}");
return hr.getUint8(0); // Heart rate: 33
}
It's creating a lot of mess to write the same code again and again to the activities where BLE data is needed.
Is there a way to only call this connected device from a single file instead of initializing the same code in every activity?
This is the link to my repo for a look at what I am doing on every activity/screen with BLE device data.
Please help me out as I am new to Flutter. Thank you
Firstly learn basic of state management using Get you can refer to my code here what happens is every time something changes or upadtes it will immediate show in UI using specific Get Widgets like Obx and GetX , these widgets listen to changes in value which are marked with obs (observable).
for exmaple :
Obx(
() => ble.isScan.value ? LinearProgressIndicator() : SizedBox(),
),
this will observe the changes in isScan value .
class BleServices extends GetxController {
FlutterBlue blue = FlutterBlue.instance;
BluetoothDevice d;
var connectedDevices = List<BluetoothDevice>().obs;
var scanResults = List<ScanResult>().obs;
var bleState = BluetoothState.off.obs;
var isScan = false.obs;
var scanRequire = false.obs;
var bluetoothServices = List<BluetoothService>().obs;
var discoveringServices = false.obs;
var characteristics = List<BluetoothCharacteristic>().obs;
#override
void onInit() async {
super.onInit();
final perb = await Permission.bluetooth.status.isGranted;
final perL = await Permission.location.status.isGranted;
if (perb && perL) {
getConnectedDevices();
} else {
await Permission.bluetooth.request();
await Permission.location.request();
}
isScanning();
state();
}
isDiscovering() async {}
getConnectedDevices() async {
final connectedDevice = await blue.connectedDevices;
connectedDevices.value = connectedDevice;
AppLogger.print('connected devices : $connectedDevice');
if (connectedDevice.length == 0) {
scanRequire.value = true;
searchDevices();
}
return connectedDevice;
}
searchDevices() {
// AppLogger.print('pppppppppppppp');
blue
.scan(timeout: Duration(seconds: 20))
.distinct()
.asBroadcastStream()
.listen((event) {
AppLogger.print(event.toString());
scanResults.addIf(!scanResults.contains(event), event);
});
Future.delayed(Duration(seconds: 20), () {
blue.stopScan();
Get.showSnackbar(GetBar(
message: 'scan is finished',
));
});
}
isScanning() {
blue.isScanning.listen((event) {
AppLogger.print(event.toString());
isScan.value = event;
});
}
state() {
blue.state.listen((event) {
AppLogger.print(event.toString());
bleState.value = event;
});
}
}

Flutter Riverpod : return 2 stream with StreamProvider

I have music player application using package Asset Audio Player and Flutter Riverpod as State Management. I want listen 2 things stream :
Listen current duration of song
Listen current song played
final currentSongPosition = StreamProvider.autoDispose<Map<String, dynamic>>((ref) async* {
final AssetsAudioPlayer player = ref.watch(globalAudioPlayers).state;
ref.onDispose(() => player.dispose());
final Stream<double> _first = player.currentPosition.map((event) => event.inSeconds.toDouble());
final Stream<double> _second =
player.current.map((event) => event?.audio.duration.inSeconds.toDouble() ?? 0.0);
final maps = {};
maps['currentDuration'] = _first;
maps['maxDurationSong'] = _second;
return maps; << Error The return type 'Map<dynamic, dynamic>' isn't a 'Stream<Map<String, dynamic>>', as required by the closure's context.
});
How can i return 2 stream into 1 StreamProvider then i can simply using like this :
Consumer(
builder: (_, watch, __) {
final _currentSong = watch(currentSongPosition);
return _currentSong.when(
data: (value) {
final _currentDuration = value['currentDuration'] as double;
final _maxDuration = value['maxDuration'] as double;
return Text('Current : $_currentDuration, Max :$_maxDuration');
},
loading: () => Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Text('Error When Playing Song'),
);
},
),
I would start by creating a StreamProvider for each Stream:
final currentDuration = StreamProvider.autoDispose<double>((ref) {
final player = ref.watch(globalAudioPlayers).state;
return player.currentPosition.map((event) => event.inSeconds.toDouble());
});
final maxDuration = StreamProvider.autoDispose<double>((ref) {
final player = ref.watch(globalAudioPlayers).state;
return player.current.map((event) => event?.audio.duration.inSeconds.toDouble() ?? 0.0);
});
Next, create a FutureProvider to read the last value of each Stream.
final durationInfo = FutureProvider.autoDispose<Map<String, double>>((ref) async {
final current = await ref.watch(currentDuration.last);
final max = await ref.watch(maxDuration.last);
return {
'currentDuration': current,
'maxDurationSong': max,
};
});
Finally, create a StreamProvider that converts durationInfo into a Stream.
final currentSongPosition = StreamProvider.autoDispose<Map<String, double>>((ref) {
final info = ref.watch(durationInfo.future);
return info.asStream();
});
This answer is another approach what #AlexHartford do.
Current solution for my case is using package rxdart CombineLatestStream.list(), this code should be look then :
StreamProvider
final currentSongPosition = StreamProvider.autoDispose((ref) {
final AssetsAudioPlayer player = ref.watch(globalAudioPlayers).state;
final Stream<double> _first = player.currentPosition.map((event) => event.inSeconds.toDouble());
final Stream<double> _second =
player.current.map((event) => event?.audio.duration.inSeconds.toDouble() ?? 0.0);
final tempList = [_first, _second];
return CombineLatestStream.list(tempList);
});
How to use it :
Consumer(
builder: (_, watch, __) {
final players = watch(globalAudioPlayers).state;
final _currentSong = watch(currentSongPosition);
return _currentSong.when(
data: (value) {
final _currentDuration = value[0];
final _maxDuration = value[1];
return Slider.adaptive(
value: _currentDuration,
max: _maxDuration,
onChanged: (value) async {
final newDuration = Duration(seconds: value.toInt());
await players.seek(newDuration);
context.read(currentSongProvider).setDuration(newDuration);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Text(error.toString()),
);
},
),
But this approach have drawback, the code is hard to readable. Especially when use in Consumer, I have to know the order of return result.