I'm working on application where I need to persist the selected date range with shared preferences. For date picker I'm using an external package, for state handling I work with provider.
At the beginning in provider model I'm reading the value from shared preferences :
class DateRangeFilterModel extends ChangeNotifier {
PickerDateRange? _dateRange;
DateRangeFilterModel() {
loadDateRange();
}
PickerDateRange? get dateRange => _dateRange;
Future<void> loadDateRange() async {
_dateRange = await SharedPreferencesManager.getFilterDateRange();
notifyListeners();
}
Future<PickerDateRange?> getDateRange() async {
return await SharedPreferencesManager.getFilterDateRange();
}
Future<void> setDateRange(PickerDateRange? dateRange) async {
_dateRange = dateRange;
await SharedPreferencesManager.saveFilterDateRange(
dateRange?.startDate, dateRange?.endDate);
notifyListeners();
}
}
With Consumer widget I try to set up the initial value, for an example I also show the result on Text widget. On Text widget saved date range appears but on SfDateRangePicker not.
Consumer<DateRangeFilterModel>(
builder: (context, model, child) => Column(
children: [
Text(model.dateRange.toString()),
SfDateRangePicker(
initialSelectedRange: model.dateRange,
showTodayButton: false,
enablePastDates: false,
selectionMode: DateRangePickerSelectionMode.range,
minDate: DateTime.now(),
onSelectionChanged: (DateRangePickerSelectionChangedArgs args) {
if (args.value is PickerDateRange) {
final DateTime? startDate = args.value.startDate;
final DateTime? endDate = args.value.endDate;
if (startDate != null && endDate != null) {
context
.read<DateRangeFilterModel>()
.setDateRange(PickerDateRange(startDate, endDate));
}
}
},
),
],
),
),
My ChangeNotifierProvider
ChangeNotifierProvider(
create: (context) => DateRangeFilterModel(),
child: const DateRangePicker()
),
I think there is some concurrency problems. Where I made mistake?
Thanks for any advice.
My solution is was to set initial range through controller.
final DateRangePickerController _controller = DateRangePickerController();
.....
SfDateRangePicker(
initialSelectedRange: _controller.selectedRange = model.dateRange,
controller: _controller,
...
)
Related
I am not able to figure out the right architecture of getting this done using firestore streams and flutter_bloc.
My goal :
I have a logged in user and I want to store the user details in the collection with the document id same as logged in user id and want to listen to changes, Throughout the program.
Meanwhile i would want to even store user details (if doesn't exist) and update user details.
My approach:
I am having a usermodel in UserState and i am listening to userModel via state using BlocBuilder.
Problem:
As i am using BlocBuilder my setState isn't working and TextFormField isn't working as it says beginBatchEdit on inactive InputConnection
Code:
UserCubit.dart
class UserCubit extends Cubit<UserState> {
final _firestore = FirebaseFirestore.instance;
final User? _currentUser = FirebaseAuth.instance.currentUser;
UserCubit() : super(UserInitialState()) {
emit(UserMainLoadingState());
_firestore --> listening to stream and updating state
.collection("sample")
.doc(_currentUser?.uid)
.snapshots()
.listen((event) {
event.exists
? emit(UserExists(sample: SampleModel.fromjson(event.data()!, event.id)))
: {emit(UserNotExists())};
});
}
Future<void> addSampleUser({required SampleModel sample}) async {
emit(UserSideLoadingState());
_firestore
.collection('sample')
.doc(_currentUser?.uid)
.set(sample.toJson())
.then((value) => emit(UserSavedUpdatedState()));
}
}
UserState.dart
abstract class UserState extends Equatable {
final SampleModel? sampleModel;
const UserState({this.sampleModel});
#override
List<Object?> get props => [sampleModel];
}
class UserExists extends UserState {
const UserExists({required SampleModel sample}) : super(sampleModel: sample);
}
Widget.dart (Save/Update User Details)
class _MyWidgetState extends State<MyWidget> {
// #override
var fullNameKey;
TextEditingController? fullNameController;
bool _formChecked = false;
TextEditingController? phoneNumberController;
Avatar? selectedAvatar;
#override
void initState() {
fullNameController = TextEditingController();
fullNameKey = GlobalKey<FormState>();
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: BlocConsumer<UserCubit, UserState>(listener: (context, state) {
if (state is UserSavedUpdatedState) {
print("Saved/Updated User");
context.goNamed(Routes.profileMain);
}
}, builder: (context, state) {
// Here i am trying to get details from the state
selectedAvatar = state.sampleModel?.avatar == "boy"
? Avatar.boy
: state.sampleModel?.avatar == "boy"
? Avatar.girl
: null;
fullNameController!.text = state.sampleModel?.name ?? "";
if (state is UserMainLoadingState) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
body: Column(
children: [
Row(
children: [
IconButton(
onPressed: () {
setState(() {
selectedAvatar = Avatar.girl; -- > This doesn't work because of BlocBuilder
});
},
icon: const Icon(Icons.girl)),
IconButton(
onPressed: () {
setState(() {
selectedAvatar = Avatar.boy; -- > This doesn't work because of BlocBuilder
});
},
icon: const Icon(Icons.boy))
],
),
Form(
key: fullNameKey,
child: TextFormField(
// BlocBuilder even freezes TextFormField
autovalidateMode: _formChecked
? AutovalidateMode.always
: AutovalidateMode.disabled,
validator: (value) {
if (value == null ||
fullNameController?.text.trim() == "") {
return "Name cannot be empty";
}
if (value.length < 3) {
return "Username must be greater than 3 characters";
}
return null;
},
controller: fullNameController,
decoration: const InputDecoration(
labelText: "Full Name",
),
)).marginDown(),
FilledButton(
onPressed: () {
setState(() {
_formChecked = true;
});
if (fullNameKey.currentState!.validate() &&
selectedAvatar != null) {
SampleModel sample = SampleModel(
name: fullNameController!.text,
avatar: selectedAvatar == Avatar.boy ? "boy" : "girl");
BlocProvider.of<UserCubit>(context)
.addSampleUser(sample: sample);
}
},
child: const Text("Submit"),
)
],
));
}),
);
}
}
As soon as the submit button is clicked it erases the entire text and validator gets activated. Avatar selection doesn't work as well.
What is the best way to achieve the desired function using streams, flutter_bloc, Suggestions would be greatly appreciated
as far as I can see you pre-select the avatar based on the emitted state. However, I do not see that you return the selection via an event/function to the bloc/cubit. So this is needed in order to send the updated avatar with the next emit.
From what I can see, I would also possibly exchange the abstract class state with a class state implementing Equatable and the simply always copyWith the state for any updates. This way you always have the same UserState - no need for if and else if for state selection, however, the data of the state changes based on the situation. I think for a user bloc/cubit this makes the lifecycle a bit easier
UPDATE:
IconButton(
onPressed: () {
setState(() {
context.read<UserCubit>.updateUser(selectedAvatar: Avatar.boy);
selectedAvatar = Avatar.boy; -- > possibly no longer needed if returned from Cubit
});
},
icon: const Icon(Icons.boy))
As for the state management, a state can look like this:
class TaskListState extends Equatable {
const TaskListState({
this.status = DataTransStatus.initial,
this.taskList = const [],
this.filter,
this.error,
this.editThisTaskId,
});
final DataTransStatus status;
final List<TaskListViewmodel> taskList;
final TaskListFilter? filter;
final String? error;
final String? editThisTaskId;
TaskListState copyWith({
DataTransStatus Function()? status,
List<TaskListViewmodel> Function()? taskList,
TaskListFilter Function()? filter,
String Function()? error,
String? Function()? editThisTaskId,
}) {
return TaskListState(
status: status != null ? status() : this.status,
taskList: taskList != null ? taskList() : this.taskList,
filter: filter != null ? filter() : this.filter,
error: error != null ? error() : this.error,
editThisTaskId: editThisTaskId != null
? editThisTaskId() : this.editThisTaskId,
);
}
#override
List<Object?> get props => [
status,
taskList,
filter,
error,
editThisTaskId,
];
}
which you use - in this case with a Stream - like this:
await emit.forEach<dynamic>(
_propertiesRepository.streamTasks(event.propertyId),
onData: (tasks) {
return state.copyWith(
status: () => DataTransStatus.success,
taskList: () => List.from(
tasks.map((t) => TaskListViewmodel.fromDomain(t))),
);
},
onError: (_, __) {
return state.copyWith(
status: () => DataTransStatus.failure,
);
},
);
Been trying to implement simple expense tracking app and got myself tangled in dependencies.
Before null-safety deleting rows was easy, now I have to use """companions""" and their concept is a bit unintuitive for me. I've been trying to upgrade this app as a learning attempt of mvvm in flutter (kinda useless knowledge) (https://github.com/musabagab/Money-Manager-Expenses-tracker-app)
Pre-ambula: they did NBBD and now auto-increment id param is marked as required and I have 0 idea how to implement deletions without knowing said ID. And moor docs are not helpful with examples, I want to delete by ID\in any way they intended to do this (https://drift.simonbinder.eu/docs/getting-started/writing_queries/#updates-and-deletes)
Before null-safety db code looked like this + generated file, and inserts\deletions worked just fine:
import 'package:moor_flutter/moor_flutter.dart';
part 'moor_database.g.dart';
class Transactions extends Table {
TextColumn get type => text()(); //expense /income
IntColumn get id => integer().autoIncrement()();
IntColumn get amount => integer()(); // 300 or 200
}
#UseMoor(tables: [Transactions], daos: [TransactionDao])
class AppDatabase extends _$AppDatabase {
AppDatabase()
: super(FlutterQueryExecutor.inDatabaseFolder(
path: "db.sqlite", logStatements: true));
int get schemaVersion => 1;
}
// Denote which tables this DAO can access
#UseDao(
tables: [Transactions],
queries: {
// this method will be generated
'getTransactionForMonth': 'SELECT * FROM transactions WHERE month = :month',
'sumTheMoneyForMonth':
'SELECT SUM(amount) FROM transactions WHERE month = :month AND type = :type'
},
)
class TransactionDao extends DatabaseAccessor<AppDatabase>
with _$TransactionDaoMixin {
final AppDatabase db;
// Called by the AppDatabase class
TransactionDao(this.db) : super(db);
Future insertTransaction(Transaction transaction) =>
into(transactions).insert(transaction);
Future deleteTransaction(Transaction transaction) =>
delete(transactions).delete(transaction);
}
and database service
import 'package:finances/core/database/moor_database.dart';
class MoorDatabaseService {
final AppDatabase _database = AppDatabase();
getAllTransactions(String month) async {
List<Transaction> allTrans = List<Transaction>();
TransactionDao transactionDao = _database.transactionDao;
allTrans = await transactionDao.getTransactionForMonth(month).get();
return allTrans;
}
Future deleteTransaction(Transaction transaction) async {
return await _database.transactionDao.deleteTransaction(transaction);
}
Future insertTransaction(Transaction transaction) async {
return await _database.transactionDao.insertTransaction(transaction);
}
insert model (yes mvvm in flutter lol)
class InsertTransactionModel extends BaseModel {
TextEditingController memoController = TextEditingController();
TextEditingController amountController = TextEditingController();
final MoorDatabaseService _moorDatabaseService =
locator<MoorDatabaseService>();
<code skipped>
Transaction newTransaction = new Transaction(
type: type,
day: selectedDay,
month: selectedMonth,
memo: memoController.text,
amount: int.parse(amount),
categoryindex: cateogryIndex);
// insert it!
await _moorDatabaseService.insertTransaction(newTransaction);
Toast.show("Added successfully!", context,
duration: Toast.LENGTH_SHORT, gravity: Toast.BOTTOM);
// return to the home
Navigator.of(context)
.pushNamedAndRemoveUntil('home', (Route<dynamic> route) => false);
}
}
insert viewmodel
class InsertTranscationView extends StatelessWidget {
final Category category;
final int selectedCategory;
InsertTranscationView(this.category, this.selectedCategory);
#override
Widget build(BuildContext context) {
return BaseView<InsertTransactionModel>(
onModelReady: (model) => model.init(selectedCategory, category.index),
builder: (context, model, child) => Scaffold(
appBar: AppBar(
title: selectedCategory == 1 ? Text('Income') : Text('Expense'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: <Widget>[
ListTile(
**<code skipped>**
Align(
alignment: Alignment.centerLeft,
child: RaisedButton(
child: Text(
'ADD',
style: TextStyle(fontSize: 16),
),
color: backgroundColor,
textColor: Colors.black,
**onPressed: () async {
await model.addTransaction(context);**
},
),
)
],
),
),
),
),
);
}
}
FIXING INSERT FOR NULL-SAFETY##
this was the easy part.
final newTransaction = TransactionsCompanion(
type: Value.ofNullable(type),
day: Value.ofNullable(selectedDay),
month: Value.ofNullable(selectedMonth),
memo: Value.ofNullable(memoController.text),
amount: Value.ofNullable(int.parse(amount)),
categoryindex: Value.ofNullable(categoryIndex));
// insert it!
await _databaseService.insertTransaction(newTransaction);
and in service
Future<int> insertTransaction(TransactionsCompanion transaction) async {
return await _database.transactionDao.insertTransaction(transaction);
}
and in database
Future<int> insertTransaction(TransactionsCompanion transaction) =>
into(transactions).insert(transaction);
Delete is a bit hard
My attempt at fixing null errors:
in delete model
Future deleteTransacation(Transaction transaction) async {
final newTransaction = TransactionsCompanion(
type: Value.ofNullable(transaction.type),
day: Value.ofNullable(transaction.day),
month: Value.ofNullable(transaction.month),
memo: Value.ofNullable(transaction.memo),
amount: Value.ofNullable(transaction.amount),
categoryindex: Value.ofNullable(transaction.categoryindex));
// delet it!
return await _databaseService.deleteTransaction(newTransaction);
}
and service
Future deleteTransaction(TransactionsCompanion transaction) async {
return await _database.transactionDao.deleteTransaction(transaction);
}
and database
Future deleteTransaction(TransactionsCompanion transaction) =>
delete(transactions).delete(transaction);
I suppose delete method in database itself is faulty, because insertions work without issues. I tried assembling TransactionCompanion and pass it as an argument into db service for it to delete same transaction by 1:1 comparision i guess?
Thanks for your efforts!
I have one bloc with multiple events. Here I load categories and locations and wait using BlocListener. But my condition for show circular progress indicator work incorrectly and after load categories and locations also shows. How I can use bloc correctly in this case?
Code
apiDataBloc.add(LoadCategoriesEvent());
apiDataBloc.add(LoadLocationsEvent());
------------------------
return BlocListener<ApiDataBloc, ApiDataState>(
listener: (context, state) {
if (state is CategoriesLoaded) {
categories = state.categories;
print("Categories loaded");
print(categories.length);
}
},
child: BlocListener<ApiDataBloc, ApiDataState>(
listener: (context, s) {
if (s is LocationsLoaded) {
locations = s.locations;
print("Locations loaded");
print(locations.length);
}
},
child: locations != null &&
categories != null &&
categories.length > 0 &&
locations.length > 0
? Container(child: Center(child: Text('Categories and locations loaded!')))
: Container(child: Center(child: CircularProgressIndicator())),
),
);
I tried also like this but doesn't work.
return BlocProvider<ApiDataBloc>(
create: (context) => apiDataBloc,
child: BlocBuilder<ApiDataBloc, ApiDataState>(
builder: (context, state) {
if (state is LocationsLoaded) {
print("Locations loaded");
locations = state.locations;
print(locations.length);
return BlocBuilder<ApiDataBloc, ApiDataState>(
builder: (context, s) {
if (s is CategoriesLoaded) {
print("Categories loaded");
categories = s.categories;
print(categories.length);
return Container(
child: Center(
child: Text('Categories and locations loaded!')));
}
return Container(
child: Center(child: CircularProgressIndicator()));
},
);
}
return Container(child: Center(child: CircularProgressIndicator()));
},
),
);
You should create one state DataLoaded with 2 fields categories and locations
Something like that:
class DataLoaded extends ApiDataState {
const DataLoaded(
this.categories,
this.locations,
);
final List<Type> categories;
final List<Type> locations;
#override
String toString() => 'DataLoaded';
}
Then you need to fetch data from API in the ApiDataBloc class:
class ApiDataBloc extends Bloc<YourEventType, ApiDataState> {
ApiDataBloc() : super(YourInitialState());
#override
Stream<ApiDataState> mapEventToState(YourEventType event) async* {
if (event is YourFetchApiEvent) {
yield YourLoadingState();
final categories = await _fetchCategories();
final locations = await _fetchLocations();
yield DataLoaded(categories,locations);
}
}
}
and the final step is BlocBuilder in your widget:
return BlocProvider<ApiDataBloc>(
create: (context) => apiDataBloc,
child: BlocBuilder<ApiDataBloc, ApiDataState>(
builder: (context, state) {
if (state is YouLoadingState) {
return Center(child: CircularProgressIndicator());
}
if (state is DataLoaded) {
print(state.locations);
print(state.categories);
return Center(
child: Text('Categories and locations loaded!'),
);
}
},
),
);
I would place the logic into the bloc. If I understand correctly, you get an event triggered as soon as the data is loaded. Then you could create 2 variables in the bloc bool categoriesLoaded, locationsLoaded which you set true upon the event. In mapEventToState you could forward from each of those event mappers to a common event mapper that checks if both variables are true and sends the proper state then. An inProgress state could display which of the data streams has already been loaded.
I know what you meant.
Example Case:
#some_bloc.dart (not in event or state file)
on<someEventNo1>((......) =>
emit(LoadingState());
emit(EmitResultAPI());
on<someEventNo2>((......) =>
emit(LoadingState());
emit(someState());
#main.dart
someMethod() {
BlocProvider.of<SomeBloc>(context).add(someEventNo1());
BlocProvider.of<SomeBloc>(context).add(someEventNo2());
}
If you do your code like that, bloc builder will not catch state change when someEventNo1 emits EmitResultAPI, because you are sending 2 consecutive BlocProvider.of<>().
Solution:
BlocProvider.of<SomeBloc>(context).add(someEventNo1());
Future.delayed(Duration(miliseconds: 100)).then((valueFuture) => BlocProvider.of<SomeBloc>(context).add(someEventNo2()));
Consider the following code:
StreamBuilder<QuerySnapshot> _createDataStream(){
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("data").limit.(_myLimit).snapshots(),
builder: (context, snapshot){
return Text(_myLimit.toString);
}
);
}
I want that the StreamBuilder refreshes when the _myLimit Variable changes.
It's possible doing it like this:
void _incrementLimit(){
setState(() =>_myLimit++);
}
My Question is if there is another, faster way, except the setState((){}); one.
Because I don't want to recall the whole build() method when the _myLimit Variable changes.
I figured out another Way but I feel like there is a even better solution because I think I don't make use of the .periodic functionality and I got a nested Stream I'm not sure how usual this is:
Stream<int> myStream = Stream.periodic(Duration(), (_) => _myLimit);
...
#override
Widget build(BuildContext context){
...
return StreamBuilder<int>(
stream: myStream,
builder: (context, snapshot){
return _createDataStream;
},
),
...
}
Solution(s)
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
int myNum = 0;
final StreamController _myStreamCtrl = StreamController.broadcast();
Stream get onVariableChanged => _myStreamCtrl.stream;
void updateMyUI() => _myStreamCtrl.sink.add(myNum);
#override
void initState() {
super.initState();
}
#override
void dispose() {
_myStreamCtrl.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
StreamBuilder(
stream: onVariableChanged,
builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
updateMyUI();
return Text(". . .");
}
return Text(snapshot.data.toString());
},
),
RaisedButton(
child: Text("Increment"),
onPressed: (){
myNum++;
updateMyUI();
},
)
],
),
)));
}
}
Some other ideas, how the StreamBuilder also could look like:
StreamBuilder(
stream: onVariableChanged,
builder: (context, snapshot){
if(snapshot.connectionState == ConnectionState.waiting){
return Text(myNum.toString());
}
return Text(snapshot.data.toString());
},
),
StreamBuilder(
stream: onVariableChanged,
initialData: myNum,
builder: (BuildContext context, AsyncSnapshot snapshot){
if(snapshot.data == null){
return Text("...");
}
return Text(snapshot.data.toString());
},
),
Declare a StreamController with broadcast, then set a friendly name to the Stream of this StreamController, then everytime you want to rebuild the wraped widget (the child of the StreamBuilder just use the sink property of the StreamController to add a new value that will trigger the StreamBuilder.
You can use StreamBuilder and AsyncSnapshot without setting the type.
But if you use StreamBuilder<UserModel> and AsyncSnapshot<UserModel> when you type snapshot.data. you will see all variables and methods from the UserModel.
final StreamController<UserModel> _currentUserStreamCtrl = StreamController<UserModel>.broadcast();
Stream<UserModel> get onCurrentUserChanged => _currentUserStreamCtrl.stream;
void updateCurrentUserUI() => _currentUserStreamCtrl.sink.add(_currentUser);
StreamBuilder<UserModel>(
stream: onCurrentUserChanged,
builder: (BuildContext context, AsyncSnapshot<UserModel> snapshot) {
if (snapshot.data != null) {
print('build signed screen, logged as: ' + snapshot.data.displayName);
return blocs.pageView.pagesView; //pageView containing signed page
}
print('build login screen');
return LoginPage();
//print('loading');
//return Center(child: CircularProgressIndicator());
},
)
This way you can use a StatelessWidget and refresh just a single sub-widget (an icon with a different color, for example) without using setState (that rebuilds the entire page).
For performance, streams are the best approach.
Edit:
I'm using BLoC architecture approach, so it's much better to declare the variables in a homePageBloc.dart (that has a normal controller class with all business logic) and create the StreamBuilder in the homePage.dart (that has a class that extends Stateless/Stateful widget and is responsible for the UI).
Edit: My UserModel.dart, you can use DocumentSnapshot instead of Map<String, dynamic> if you are using Cloud Firestore database from Firebase.
class UserModel {
/// Document ID of the user on database
String _firebaseId = "";
String get firebaseId => _firebaseId;
set firebaseId(newValue) => _firebaseId = newValue;
DateTime _creationDate = DateTime.now();
DateTime get creationDate => _creationDate;
DateTime _lastUpdate = DateTime.now();
DateTime get lastUpdate => _lastUpdate;
String _displayName = "";
String get displayName => _displayName;
set displayName(newValue) => _displayName = newValue;
String _username = "";
String get username => _username;
set username(newValue) => _username = newValue;
String _photoUrl = "";
String get photoUrl => _photoUrl;
set photoUrl(newValue) => _photoUrl = newValue;
String _phoneNumber = "";
String get phoneNumber => _phoneNumber;
set phoneNumber(newValue) => _phoneNumber = newValue;
String _email = "";
String get email => _email;
set email(newValue) => _email = newValue;
String _address = "";
String get address => _address;
set address(newValue) => _address = newValue;
bool _isAdmin = false;
bool get isAdmin => _isAdmin;
set isAdmin(newValue) => _isAdmin = newValue;
/// Used on first login
UserModel.fromFirstLogin() {
_creationDate = DateTime.now();
_lastUpdate = DateTime.now();
_username = "";
_address = "";
_isAdmin = false;
}
/// Used on any login that isn't the first
UserModel.fromDocument(Map<String, String> userDoc) {
_firebaseId = userDoc['firebaseId'] ?? '';
_displayName = userDoc['displayName'] ?? '';
_photoUrl = userDoc['photoUrl'] ?? '';
_phoneNumber = userDoc['phoneNumber'] ?? '';
_email = userDoc['email'] ?? '';
_address = userDoc['address'] ?? '';
_isAdmin = userDoc['isAdmin'] ?? false;
_username = userDoc['username'] ?? '';
//_lastUpdate = userDoc['lastUpdate'] != null ? userDoc['lastUpdate'].toDate() : DateTime.now();
//_creationDate = userDoc['creationDate'] != null ? userDoc['creationDate'].toDate() : DateTime.now();
}
void showOnConsole(String header) {
print('''
$header
currentUser.firebaseId: $_firebaseId
currentUser.username: $_username
currentUser.displayName: $_displayName
currentUser.phoneNumber: $_phoneNumber
currentUser.email: $_email
currentUser.address: $_address
currentUser.isAdmin: $_isAdmin
'''
);
}
String toReadableString() {
return
"displayName: $_displayName; "
"firebaseId: $_firebaseId; "
"email: $_email; "
"address: $_address; "
"photoUrl: $_photoUrl; "
"phoneNumber: $_phoneNumber; "
"isAdmin: $_isAdmin; ";
}
}
As you can see in first part I'm checking that a certain value contains in a document from Firestore and returns a boolean value. Now I'm calling that function in a build and based on that return value I'm changing a chip color (second part).
Now the problem is maybe because I'm calling it in a build function so its being called continuously and on that build and it costing me a ton of reads in Firestore or maybe the function is inefficient. How can I write this more efficiently?
checkAtt(String name, id , date) async{
var ref = _db.collection('subjects').document(id).collection('Att').document(date);
var docref = await ref.get();
return docref.data.containsKey(name)
?true
:false;
}
class PresentChip extends StatefulWidget {
final candidate;
PresentChip(
this.candidate, {
Key key,
}) : super(key: key);
#override
_PresentChipState createState() => _PresentChipState();
}
class _PresentChipState extends State<PresentChip> {
var isSelected = false;
var c = false;
#override
Widget build(BuildContext context) {
final SelectSub selectSub = Provider.of<SelectSub>(context);
final Date date = Provider.of<Date>(context);
db.checkAtt(widget.candidate, selectSub.selectsub, date.datenew).then((result){
print(result);
setState(() {
c = result;
});
});
return Container(
child: ChoiceChip(
label: Text('Present'),
selected: isSelected,
onSelected: (selected) {
db.gibAtt(
widget.candidate, selectSub.selectsub, date.datenew.toString());
setState(() {
isSelected = selected;
});
},
backgroundColor: !c ?Colors.red :Colors.green ,
selectedColor: !c ?Colors.red :Colors.green ,
));
}
}
Assuming you only want to read once from firestore, you need a FutureBuilder.
return Container(
child: FutureBuilder(
future: db.checkAtt(widget.candidate, selectSub.selectsub, date.datenew),
builder: (context, snapshot) {
if(snapshot.hasData)
return ChoiceChip(
...
backgroundColor: !snapshot.data ?Colors.red :Colors.green,
selectedColor: !snapshot.data ?Colors.red :Colors.green,
);
//Return another widget if the future has no data
return Text('Future has no data');
}
)
);
If you need your UI to react to changes from firestore, use a StreamBuilder.
You can remove the following bloc from your build method:
db.checkAtt(widget.candidate, selectSub.selectsub, date.datenew).then((result){
print(result);
setState(() {
c = result;
});
});