is there a better way to write a document stream builder? - flutter

Is there a better way to write this code?
The application is about jobs and job applications.
Each job can have multiple job applications.
if the job is fulfilled, all the job applications should be closed so that people are not applying to a job that is closed.
I don't like the way the code is written but it achieves the functionality that I wanted.
to get to the 'JobApplicationView' I have to go through a page that displays all the current job applications, once I click on one of the job application, the job application.
here is the snip of code from the 'job application list view'
StreamBuilder(
stream: _jobsService.allJobApplications(userId),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.active:
if (snapshot.hasData) {
final allJobApplications =
snapshot.data as Iterable<CloudJobApplication>;
return JobApplicationsListView(
allowScroll: false,
jobApplications: allJobApplications,
onTap: (job) {
Navigator.of(context).pushNamed(
myJobApplicationsRoute,
arguments: job,
);
},
);
} else {
return const CircularProgressIndicator();
}
default:
return const CircularProgressIndicator();
}
},
),
Below is the really ugly code that performs the functionality that i want
class JobApplicationView extends StatefulWidget {
const JobApplicationView({Key? key}) : super(key: key);
#override
_JobApplicationViewState createState() => _JobApplicationViewState();
}
class _JobApplicationViewState extends State<JobApplicationView> {
final _formKey = GlobalKey<FormState>();
late final FirebaseCloudStorage _cloudFunctions;
final currentUser = AuthService.firebase().currentUser!;
#override
void initState() {
super.initState();
_cloudFunctions = FirebaseCloudStorage();
}
getExistingJobApplication(BuildContext context) {
return FirebaseFirestore.instance
.collection('job application')
.doc(context.getArgument<CloudJobApplication>()!.documentId)
.snapshots();
}
submitProposal(localStates) {
submitProposal() async {
await _cloudFunctions.updateJobApplicationColumn(
documentId: context.getArgument<CloudJobApplication>()!.documentId,
fieldNameColumn: jobApplicationStateColumn,
fieldNameColumnValue: jobApplicationStateOpen);
await _cloudFunctions.updateJobApplicationColumn(
documentId: context.getArgument<CloudJobApplication>()!.documentId,
fieldNameColumn: jobApplicationSubStateColumn,
fieldNameColumnValue: 'Awaiting client proposal');
}
// job applicator can submit proposal if the state is new
if (localStates['jobApplicatotIsSameAsCurrentUser'] &&
localStates['jobApplication'][jobApplicationStateColumn] ==
jobApplicationStateNew) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(fontSize: 13),
backgroundColor: Colors.blue,
),
onPressed: () => submitProposal(),
child: const Text('Submit proposal'),
);
// job creator can accept the proposal if state is open
// job creator can deny job proposal if state is open
} else {
return Container();
}
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('update job application'),
actions: [],
),
body: StreamBuilder(
stream: getExistingJobApplication(context),
builder: (context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
// this is where I saved the local states
var localStates = {};
localStates['width'] = MediaQuery.of(context).size.width;
localStates['jobApplication'] = snapshot.data;
localStates['formFieldsEditable'] = localStates['jobApplication']
[jobApplicationStateColumn] ==
jobApplicationStateNew &&
currentUser.id ==
localStates['jobApplication'][jobApplicatorIdColumn]
? true
: false;
localStates['jobApplicatotIsSameAsCurrentUser'] = currentUser.id ==
localStates['jobApplication'][jobApplicatorIdColumn];
localStates['jobCreatorIsSameAsCurrentUser'] = currentUser.id ==
localStates['jobApplication'][jobCreatorIdColumn];
return Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(32.0),
children: [
submitProposal(localStates),
],
),
);
}),
);
}
}

Related

incoming data from server not updating local state fields

I have a view with a list of job applications,
when you click on one of the job applications, it sends the context to the next page, where you can edit the job application.
when I click a job application, it sends its context to the next page I think. it then populates the local variables I think.
the goal is to update the job application on the server, when the change is recorded, I should get the new data back from the server, and it should then update the local variables. right now I only have the button 'submit inquiry' to make the code easy to follow.
Below is the view that has all job applications
class JobApplicationsView extends StatefulWidget {
const JobApplicationsView({Key? key}) : super(key: key);
#override
_JobApplicationsViewState createState() => _JobApplicationsViewState();
}
class _JobApplicationsViewState extends State<JobApplicationsView> {
late final FirebaseCloudStorage _jobsService;
String get userId => AuthService.firebase().currentUser!.id;
#override
void initState() {
_jobsService = FirebaseCloudStorage();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Your job applications'),
actions: [
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('Log out'),
),
];
},
)
],
),
body: StreamBuilder(
stream: _jobsService.allJobApplications(userId),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
case ConnectionState.active:
if (snapshot.hasData) {
final allJobApplications =
snapshot.data as Iterable<CloudJobApplication>;
return JobApplicationsListView(
allowScroll: false,
jobApplications: allJobApplications,
onTap: (job) {
Navigator.of(context).pushNamed(
myJobApplicationsRoute,
arguments: job,
);
},
);
} else {
return const CircularProgressIndicator();
}
default:
return const CircularProgressIndicator();
}
},
),
);
}
}
the function used to get all job applications.
final jobApplication = FirebaseFirestore.instance.collection('job application');
Stream<Iterable<CloudJobApplication>> allJobApplications(userId) =>
jobApplication.snapshots().map((event) => event.docs
.map((doc) => CloudJobApplication.fromSnapshot(doc))
.where((jobApplication) => jobApplication.jobApplicatorId == userId));
here is the view that allows the job application to be edited:
The function 'getExistingJobApplication' is where the issue might be.
class JobApplicationView extends StatefulWidget {
const JobApplicationView({Key? key}) : super(key: key);
#override
_JobApplicationViewState createState() => _JobApplicationViewState();
}
// https://youtu.be/VPvVD8t02U8?t=90350
class _JobApplicationViewState extends State<JobApplicationView> {
CloudJobApplication? _jobApplication;
late final FirebaseCloudStorage _cloudFunctions;
final _formKey = GlobalKey<FormState>();
final currentUser = AuthService.firebase().currentUser!;
// state varibles
String _jobApplicationState = 'default';
String? _jobApplicationSubState;
String? _jobCreatorId;
String? _jobApplicatorId;
String? _jobApplicationEstimate;
late Timestamp? _jobApplicationStartDate;
late Timestamp? _jobApplicationEndDate;
late final TextEditingController _jobDescriptionController;
bool? _formFieldsEditable;
#override
void initState() {
super.initState();
_cloudFunctions = FirebaseCloudStorage();
_jobDescriptionController = TextEditingController();
}
Future<CloudJobApplication> getExistingJobApplication(
BuildContext context) async {
// if the local state variables are default, use the information from the context
// if the local state variables are not default, that means an update has occur on the server. fetch the new data from the server
var widgetJobApplication;
if (_jobApplicationState == 'default') {
log('using job application from context');
widgetJobApplication = context.getArgument<CloudJobApplication>();
_jobApplication = widgetJobApplication;
_jobApplicationState =
widgetJobApplication?.jobApplicationState as String;
_jobApplicationEstimate = widgetJobApplication.jobApplicationEstimate;
_jobDescriptionController.text =
widgetJobApplication?.jobApplicationDescription as String;
_jobApplicationStartDate = widgetJobApplication.jobApplicationStartDate;
_jobApplicationEndDate = widgetJobApplication.jobApplicationEndDate;
_jobCreatorId = widgetJobApplication.jobCreatorId;
_jobApplicatorId = widgetJobApplication.jobApplicatorId;
_formFieldsEditable = _jobApplicationState == jobApplicationStateNew ||
_jobApplicationState == jobApplicationStateOpen
? true
: false;
} else {
log('using job application from server');
widgetJobApplication =
_cloudFunctions.getJobApplication(_jobApplication!.documentId);
}
return widgetJobApplication;
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('update job application'),
actions: [],
),
body: FutureBuilder(
future: getExistingJobApplication(context),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.done:
return Form(
key: _formKey,
child: ListView(padding: const EdgeInsets.all(32.0), children: [
JobApplicationStateVariablesWidget(
jobApplicationState: _jobApplicationState),
const Divider(
height: 20,
thickness: 5,
indent: 0,
endIndent: 0,
color: Colors.blue,
),
ElevatedButton(
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(fontSize: 13),
backgroundColor: false ? Colors.black : Colors.blue,
),
// HERE IS BUTTON AT PLAY
onPressed: () async {
setState(() {
_jobApplicationState = jobApplicationStatePending;
});
await _cloudFunctions.updateJobApplicationColumn(
documentId: _jobApplication?.documentId as String,
fieldNameColumn: jobApplicationStateColumn,
fieldNameColumnValue: jobApplicationStatePending);
},
child: const Text('Submit inquiry'),
),
]),
);
default:
return const CircularProgressIndicator();
}
},
),
);
}
}
The way its set up right now works but in a crappy way...
for example: if the job was closed, then the job application should become closed. the way I have the code right now wont work with that apprach because I am hitting the local state variables directly

read user info from FirebaseFirestore in StreamBuilder

I'm trying to read document info from Database this my code
abstract class Database{
Future<void> sendText(SendOrder textData);
Stream<QuerySnapshot<Map<String, dynamic>>> readDataStream();
}
class FirestoreDatabase implements Database{
FirestoreDatabase({required this.uid});
final String uid;
#override
Future<void> sendText(SendOrder textData) async {
final documentRefer = FirebaseFirestore.instance.collection('userInfo').doc(uid);
await documentRefer.set(textData.toMap());
}
#override
Stream<QuerySnapshot<Map<String, dynamic>>> readDataStream() {
return FirebaseFirestore.instance.collection('userInfo').where('uid', isEqualTo: uid).snapshots();
}
}
with send text I got no problem but read data I got problem
getData() async{
return database.readDataStream().map((snapshot) => snapshot.docs);
}
Future<void> _dataSend(BuildContext context) async {
await database.sendText(SendOrder(userName: 'name', coins: '12', level: '12'));
}
body: StreamBuilder<Database>(
builder: (context, snapshot){
return Column(
children: [
ElevatedButton(onPressed: () {
_dataSend(context);
}, child:
const Text('Click')),
Text(
getData().toString()
, style: const TextStyle(color: Colors.white),
)
],
);
},
)
I need to read map data but I have problem becuase I'm new flutter developer , don't know how to convert data or where comes from
so how to read data?
Is my code right to read as StreamBuilder?
You had not set stream for your StreamBuilder. Check below code for simple StreamBuilder:
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
Stream<String> _generateNumbers() async* {
for (int i = 1; i <= 10; i++) {
await Future<void>.delayed(const Duration(seconds: 1));
yield "$i";
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: StreamBuilder<String>(
stream: _generateNumbers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.active
|| snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Text(
snapshot.data ?? '',
style: const TextStyle(color: Colors.red, fontSize: 40)
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
},
),
),
);
}
}

Flutter Navigator 2.0 pages using with showGeneralDialog to handle clearing pages

I am trying to develop a logout feature with Navigator 2.0 pages & showGeneralDialog. A dialog (created by showGeneralDialog) will handle the logout after a button in the dialog is clicked by the user and the dialog is closed. However, an error is thrown in _RouteEntry.markForComplete (the error is thrown by the assert statement).
I tried to create a dummy project with some simple code:
import 'package:flutter/material.dart';
void main() {
runApp(BooksApp());
}
class Book {
final String title;
final String author;
Book(this.title, this.author);
}
class BooksApp extends StatefulWidget {
#override
State<StatefulWidget> createState() => _BooksAppState();
}
class _BooksAppState extends State<BooksApp> {
BookRouterDelegate _routerDelegate = BookRouterDelegate();
BookRouteInformationParser _routeInformationParser =
BookRouteInformationParser();
PlatformRouteInformationProvider _platformRouteInformationProvider =
PlatformRouteInformationProvider(
initialRouteInformation: RouteInformation(location: '/'));
#override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Books App',
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
routeInformationProvider: _platformRouteInformationProvider,
);
}
}
class BookRouteInformationParser extends RouteInformationParser<RoutePath> {
#override
Future<RoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location);
if (uri.pathSegments.length >= 2) {
var remaining = uri.pathSegments[1];
return RoutePath.details(int.tryParse(remaining));
} else if (uri.pathSegments.length > 0 && uri.pathSegments[0] == 'book') {
return RoutePath.home();
} else
return RoutePath.login();
}
#override
RouteInformation restoreRouteInformation(RoutePath path) {
if (path.isLogin) return RouteInformation(location: '/');
if (path.isHomePage) {
return RouteInformation(location: '/book');
}
if (path.isDetailsPage) {
return RouteInformation(location: '/book/${path.id}');
}
return null;
}
}
class BookRouterDelegate extends RouterDelegate<RoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<RoutePath> {
final GlobalKey<NavigatorState> navigatorKey;
Book _selectedBook;
List<Book> books = [
Book('Stranger in a Strange Land', 'Robert A. Heinlein'),
Book('Foundation', 'Isaac Asimov'),
Book('Fahrenheit 451', 'Ray Bradbury'),
];
BookRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
bool _showLogin = false;
RoutePath get currentConfiguration => _showLogin
? RoutePath.login()
: _selectedBook == null
? RoutePath.home()
: RoutePath.details(books.indexOf(_selectedBook));
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
if (currentConfiguration.isLogin)
MaterialPage(
key: ValueKey('LoginPage'),
child: Scaffold(
appBar: AppBar(
title: Text('Login'),
),
),
),
if (!currentConfiguration.isLogin)
MaterialPage(
key: ValueKey('BooksListPage'),
child: BooksListScreen(
books: books,
onTapped: _handleBookTapped,
),
),
if (_selectedBook != null) BookDetailsPage(book: _selectedBook)
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// Update the list of pages by setting _selectedBook to null
_selectedBook = null;
notifyListeners();
return true;
},
);
}
#override
Future<void> setNewRoutePath(RoutePath path) async {
if (path.isDetailsPage) {
_selectedBook = books[path.id];
}
}
void _handleBookTapped(Book book) {
_selectedBook = book;
notifyListeners();
}
void handleLogout() {
_showLogin = true;
_selectedBook = null;
notifyListeners();
}
}
class BookDetailsPage extends Page {
final Book book;
BookDetailsPage({
this.book,
}) : super(key: ValueKey(book));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this,
builder: (BuildContext context) {
return BookDetailsScreen(book: book);
},
);
}
}
class RoutePath {
final bool isLogin;
final int id;
RoutePath.login()
: id = null,
isLogin = true;
RoutePath.home()
: id = null,
isLogin = false;
RoutePath.details(this.id) : isLogin = false;
bool get isHomePage => id == null;
bool get isDetailsPage => id != null;
}
class BooksListScreen extends StatelessWidget {
final List<Book> books;
final ValueChanged<Book> onTapped;
BooksListScreen({
#required this.books,
#required this.onTapped,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView(
children: [
for (var book in books)
ListTile(
title: Text(book.title),
subtitle: Text(book.author),
onTap: () => onTapped(book),
)
],
),
);
}
}
class BookDetailsScreen extends StatelessWidget {
final Book book;
BookDetailsScreen({
#required this.book,
});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: () async {
final response = await showGeneralDialog<bool>(
context: context,
useRootNavigator: true,
barrierDismissible: false,
transitionDuration: const Duration(milliseconds: 300),
transitionBuilder: (context, animation, __, child) {
return ScaleTransition(
scale: animation,
child: child,
);
},
pageBuilder: (context, _, __) => _CustomDialog(),
);
if (response == null) return;
if (response) {
// await Future.delayed(const Duration(milliseconds: 300));
context
.findAncestorStateOfType<_BooksAppState>()
._routerDelegate
.handleLogout();
}
},
),
],
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (book != null) ...[
ListTile(
title: Text(book.title),
subtitle: Text(book.author,
style: Theme.of(context).textTheme.subtitle1),
),
],
],
),
),
);
}
}
class _CustomDialog extends StatelessWidget {
_CustomDialog({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Clear all pages?'),
RaisedButton(
child: Text('OK'),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop(true);
},
),
],
),
),
);
}
}
However, in this dummy project, the error is randomly thrown, sometimes it is thrown at NavigatorState.finalizeRoute (in this assert statement: assert(_history.where(_RouteEntry.isRoutePredicate(route)).length == 1);), and sometimes it is thrown at exactly the same as the one I mention which is _RouteEntry.markForComplete.
The workaround I can find currently is to delay until the dialog pop transition is completed (delay before context.findAncestorStateOfType<_BooksAppState>()._routerDelegate.handleLogout();).
However, I would like to know the proper fix for this instead of waiting for it to completely pop because I am unsure if there is any hidden trouble I might face.
The issue seems to be fixed on the issue ticket you've filed. No workarounds needed. Just update the Flutter SDK version to fix the issue.

Flutter screen navigating back when updating firestore document

I'm have successfully displayed list of users in ListView using StreamBuilder. But when I'm updating user document in firestore, screen in my mobile app is automatically navigating back.
This is my screens flow. Login -> Home -> ManageUsers -> UserDetails.
By using below code, I created a list in Manage Users screen. Now I'm trying to update user first name in firebase console. After updating the data ManageUsers screen is closing.
Screen Rec
Widget build(BuildContext context) {
return new StreamBuilder(
stream: Firestore.instance.collection('users').snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData)
return Center(
child: CircularProgressIndicator(),
);
if (snapshot.hasError)
return Center(child: new Text('Error: ${snapshot.error}'));
final int itemsCount = snapshot.data.documents.length;
switch (snapshot.connectionState) {
case ConnectionState.none:
// TODO: Handle this case.
return new CircularProgressIndicator();
break;
case ConnectionState.waiting:
// TODO: Handle this case.
return new CircularProgressIndicator();
break;
default:
return new ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: itemsCount,
addAutomaticKeepAlives: true,
itemBuilder: (BuildContext context, int index) {
final DocumentSnapshot document =
snapshot.data.documents[index];
return new ListTile(
title: new Text(document['first_name']),
subtitle: new Text(document['last_name']),
onTap: () => {openUserDetailsScreen(document, context)},
);
},
);
}
},
);
}
Actually it should refresh the data in the same screen instead of navigating back. Am I doing anything wrong in building the list.
Home Screen Code
class HomeScreen extends StatefulWidget {
HomeScreen({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomeScreenState createState() => _MyHomeScreenState();
}
class _MyHomeScreenState extends State<HomeScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
automaticallyImplyLeading: false,
leading: Builder(
builder: (context) =>
IconButton(
icon: new Icon(Icons.menu),
onPressed: () => Scaffold.of(context).openDrawer(),
),
),
),
drawer: MyDrawer(widget.title),
body: Center(
child: Text("Home Screen"),
),
);
}
#override
void initState() {
print('in home page ${globals.loggedInUser.firstName}');
}
}
From home page I'm navigating to ManageUsers screen via drawer. Here is the code for drawer.
class MyDrawer extends StatelessWidget {
MyDrawer(this.currentPage);
final String currentPage;
bool isAdmin = true;
#override
Widget build(BuildContext context) {
var currentDrawer = Provider.of<DrawerStateInfo>(context).getCurrentDrawer;
return Drawer(
child: ListView(
children: <Widget>[
_CustomListTile(
currentPage, globals.HOME_MENU_TITLE, currentDrawer),
_CustomListTile(
currentPage, globals.LOGIN_MENU_TITLE, currentDrawer),
ConditionalBuilder(
condition: isAdmin,
builder: (context) => _CustomListTile(currentPage,
globals.MANAGE_USERS_MENU_TITLE, currentDrawer),
)
],
),
);
}
}
class _CustomListTile extends StatelessWidget {
final String currentPage;
final String tileTitle;
final currentDrawer;
_CustomListTile(this.currentPage, this.tileTitle, this.currentDrawer);
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(
tileTitle,
style: currentDrawer == 1
? TextStyle(fontWeight: FontWeight.bold)
: TextStyle(fontWeight: FontWeight.normal),
),
onTap: () {
Navigator.of(context).pop();
if (this.currentPage == tileTitle) return;
Provider.of<DrawerStateInfo>(context).setCurrentDrawer(1);
switch (tileTitle) {
case globals.HOME_MENU_TITLE:
{
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (BuildContext context) => HomeScreen(
title: globals.HOME_MENU_TITLE,
)));
break;
}
case globals.LOGIN_MENU_TITLE:
{
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (BuildContext context) => LoginScreen(
title: globals.LOGIN_MENU_TITLE,
)));
break;
}
case globals.MANAGE_USERS_MENU_TITLE:
{
Navigator.of(context).pushNamed("/ManageUsers");
break;
}
default:
{
break;
}
}
});
}
}
ListView StreamBuilder means it will listening to your collection.
Please remove the listener when you move to next screen.
This piece of code looks wrong to me:
Provider.of<DrawerStateInfo>(context).setCurrentDrawer(1);
You should have something like an enum or even the tileTitle to use as the saved state for the currently selected option on the drawer, otherwise, you only know there is a selected option, but not exactly which one.
This leads you to this crazy behavior of calling incorrect routes.
Try something like this
class MyDrawer extends StatelessWidget {
MyDrawer(this.currentPage);
final String currentPage;
bool isAdmin = true;
#override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
children: <Widget>[
_CustomListTile(currentPage, globals.HOME_MENU_TITLE),
_CustomListTile(currentPage, globals.LOGIN_MENU_TITLE),
isAdmin
? _CustomListTile(currentPage, globals.MANAGE_USERS_MENU_TITLE)
: Container(),
],
),
);
}
}
class _CustomListTile extends StatelessWidget {
final String currentPage;
final String tileTitle;
_CustomListTile(
this.currentPage,
this.tileTitle,
);
#override
Widget build(BuildContext context) {
return Consumer<DrawerStateInfo>(
builder: (context, draweStateInfo, _) {
final currentSelectedItem = draweStateInfo.getCurrentDrawer();
return ListTile(
title: Text(
tileTitle,
style: currentSelectedItem == tileTitle
? TextStyle(fontWeight: FontWeight.bold)
: TextStyle(fontWeight: FontWeight.normal),
),
onTap: () {
Navigator.of(context).pop();
if (currentSelectedItem == tileTitle) return;
draweStateInfo.setCurrentDrawer(tileTitle);
switch (currentSelectedItem) {
case globals.HOME_MENU_TITLE:
{
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (BuildContext context) => HomeScreen(
title: globals.HOME_MENU_TITLE,
)));
break;
}
case globals.LOGIN_MENU_TITLE:
{
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (BuildContext context) => LoginScreen(
title: globals.LOGIN_MENU_TITLE,
)));
break;
}
case globals.MANAGE_USERS_MENU_TITLE:
{
Navigator.of(context).pushNamed("/ManageUsers");
break;
}
default:
{
break;
}
}
});
},
);
}
}

How to use flutter provider in a statefulWidget?

I am using flutter_provider for state management. I want to load some items on page(statefulwidget) load from Api. I am showing a loader on page start and want to show the items once they are fetched.
PlayList.dart -
class Playlist extends StatefulWidget {
#override
_PlaylistState createState() => _PlaylistState();
}
class _PlaylistState extends State<Playlist> {
var videosState;
#override
void initState() {
super.initState();
videosState = Provider.of<VideosProvider>(context);
videosState.fetchVideosList();
}
#override
Widget build(BuildContext context) {
var videos = videosState.playlist;
return Scaffold(
appBar: AppBar(
title: Text('My Videos'),
),
body: RefreshIndicator(
child: Container(
width: double.infinity,
height: double.infinity,
child: videos.length
? ListView.builder(
itemBuilder: (BuildContext context, index) {
return _videoListItem(context, index, videos, videosState);
},
itemCount: videos.length,
)
: Center(
child: CircularProgressIndicator(),
),
),
onRefresh: () => null,
),
);
}
}
My provider is like this -
class VideosProvider with ChangeNotifier {
List _playlist;
int _currentVideoId;
get playlist => _playlist;
void setPlayList(videosList) {
_playlist = videosList;
}
Future fetchVideosList() async {
http.Response response =
await http.get("http://192.168.1.22:3000/videos-list/");
print(json.decode(response.body));
videos = json.decode(response.body)["data"];
setPlayList(videos);
return videos;
}
}
This gives an error of -
inheritFromWidgetOfExactType(_Provider<VideosProvider>) or inheritFromElement() was called before _PlaylistState.initState() completed.
here is the build method of the parent of playList class, wrapped in a changenotifier,
Widget build(BuildContext context) {
return ChangeNotifierProvider<VideosProvider>(
builder: (BuildContext context) => VideosProvider(),
child: MaterialApp(
title: "My App",
home: new Playlist(),
),
);
}
So, all the examples on flutter_provider on internet show usage of provider on a statelesswidget, where state changes occur on user interactions like a button click. None about how to use provider in a statefulWidget, and cases where data has to be updated on page load without any interaction.
I am aware of streambuilder and futurebuilder for this kind of scenarios, but want to understand how this can be done with flutter_provider. How can I use provider to call fetchVideosList in initState(on pageload)? Does this case can/should be handled with a statelessWidget?
Does this case can/should be handled with a statelessWidget?
The answer is : No, it does not
I am heavy user of StatefulWidget + Provider. I always use this pattern for displaying a Form which contains fields, that available for future edit or input.
Updated : February 9 2020
Regarding to Maks comment, I shared better way to manage provider using didChangeDependencies.
You may check to this github repository
main.dart
First Step
Initiate PlayListScreen inside ChangeNotifierProvider
class PlaylistScreenProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<VideosProvider>(
create: (_) {
return VideosProvider();
},
child: PlaylistScreen(),
);
}
}
class MainScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Screen'),
),
body: Center(
child: RaisedButton(
child: Text("Go To StatefulWidget Screen"),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) {
return PlaylistScreenProvider();
},
),
);
},
),
),
);
}
}
Second Step
Make PlaylistScreen as Stateful Widget to hold TextEditingContoller
and other values.
playlistScreen.dart
class PlaylistScreen extends StatefulWidget {
#override
_PlaylistScreenState createState() => _PlaylistScreenState();
}
class _PlaylistScreenState extends State<PlaylistScreen> {
List _playlistList;
String _errorMessage;
Stage _stage;
final _searchTextCtrl = TextEditingController();
#override
void dispose() {
super.dispose();
_searchTextCtrl.dispose();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
final videosState = Provider.of<VideosProvider>(context);
_playlistList = videosState.playlist;
_stage = videosState.stage;
_errorMessage = videosState.errorMessage;
}
void actionSearch() {
String text = _searchTextCtrl.value.text;
Provider.of<VideosProvider>(context, listen: false)
.updateCurrentVideoId(text);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('My Videos'),
),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: <Widget>[
Container(
child: RaisedButton.icon(
icon: Icon(Icons.search),
label: Text("Filter"),
onPressed: () {
actionSearch();
},
),
),
Container(
child: TextField(
controller: _searchTextCtrl,
onSubmitted: (value) {
actionSearch();
},
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Please input 1 or 2',
),
),
),
Flexible(
child: _stage == Stage.DONE
? PlaylistTree(_playlistList)
: _stage == Stage.ERROR
? Center(child: Text("$_errorMessage"))
: Center(
child: CircularProgressIndicator(),
),
)
],
),
),
);
}
}
class PlaylistTree extends StatelessWidget {
PlaylistTree(this.playlistList);
final List playlistList;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: playlistList.length,
itemBuilder: (context, index) {
var data = playlistList[index];
return Container(
child: Text("${data['id']} - ${data['first_name']}"),
);
},
);
}
}
Last Step
make provider to handle Business Logic
videosProvider.dart
enum Stage { ERROR, LOADING, DONE }
class VideosProvider with ChangeNotifier {
String errorMessage = "Network Error";
Stage stage;
List _playlist;
int _currentVideoId;
VideosProvider() {
this.stage = Stage.LOADING;
initScreen();
}
void initScreen() async {
try {
await fetchVideosList();
stage = Stage.DONE;
} catch (e) {
stage = Stage.ERROR;
}
notifyListeners();
}
List get playlist => _playlist;
void setPlayList(videosList) {
_playlist = videosList;
}
void validateInput(String valueText) {
if (valueText == ""){
this._currentVideoId = null;
return;
}
try {
int valueInt = int.parse(valueText);
if (valueInt == 1 || valueInt == 2){
this._currentVideoId = valueInt;
}
else {
this.errorMessage = "Use only 1 and 2";
throw 1;
}
} on FormatException catch (e) {
this.errorMessage = "Must be a number";
throw 1;
}
}
void updateCurrentVideoId(String value) async {
this.stage = Stage.LOADING;
notifyListeners();
try {
validateInput(value);
await fetchVideosList();
stage = Stage.DONE;
} on SocketException catch (e) {
this.errorMessage = "Network Error";
stage = Stage.ERROR;
} catch (e) {
stage = Stage.ERROR;
}
notifyListeners();
}
Future fetchVideosList() async {
String url;
if (_currentVideoId != null) {
url = "https://reqres.in/api/users?page=$_currentVideoId";
} else {
url = "https://reqres.in/api/users";
}
http.Response response = await http.get(url);
var videosList = json.decode(response.body)["data"];
setPlayList(videosList);
}
}
Old answer : Aug 19 2019
In my case :
form_screen.dart
class Form extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<FormProvider>(
builder: (_) {
return FormProvider(id: ...); // Passing Provider to child widget
},
child: FormWidget(), // So Provider.of<FormProvider>(context) can be read here
);
}
}
class FormWidget extends StatefulWidget {
#override
_FormWidgetState createState() => _FormWidgetState();
}
class _FormWidgetState extends State<FormWidget> {
final _formKey = GlobalKey<FormState>();
// No need to override initState like your code
#override
Widget build(BuildContext context) {
var formState = Provider.of<FormProvider>(context) // access any provided data
return Form(
key: _formKey,
child: ....
);
}
}
FormProvider as a class, need to update their latest value from API. So, initially, it will request to some URL and updates corresponding values.
form_provider.dart
class FormProvider with ChangeNotifier {
DocumentModel document;
int id;
FormProvider({#required int id}) {
this.id = id;
initFormFields(); // will perform network request
}
void initFormFields() async {
Map results = initializeDataFromApi(id: id);
try {
document = DocumentModel.fromJson(results['data']);
} catch (e) {
// Handle Exceptions
}
notifyListeners(); // triggers FormWidget to re-execute build method for second time
}
In your case :
PlayList.dart
class PlaylistScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<VideosProvider>(
builder: (_) {
return VideosProvider(); // execute construct method and fetchVideosList asynchronously
},
child: Playlist(),
);
}
}
class Playlist extends StatefulWidget {
#override
_PlaylistState createState() => _PlaylistState();
}
class _PlaylistState extends State<Playlist> {
final _formKey = GlobalKey<FormState>();
#override
void initState() {
super.initState();
// We *moved* this to build method
// videosState = Provider.of<VideosProvider>(context);
// We *moved* this to constructor method in provider
// videosState.fetchVideosList();
}
#override
Widget build(BuildContext context) {
// Moved from initState
var videosState = Provider.of<VideosProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text('My Videos'),
),
body: RefreshIndicator(
}
}
provider.dart
class VideosProvider with ChangeNotifier {
VideosProvider() {
// *moved* from Playlist.initState()
fetchVideosList(); // will perform network request
}
List _playlist;
int _currentVideoId;
get playlist => _playlist;
void setPlayList(videosList) {
_playlist = videosList;
}
Future fetchVideosList() async {
http.Response response =
await http.get("http://192.168.1.22:3000/videos-list/");
print(json.decode(response.body));
videos = json.decode(response.body)["data"];
setPlayList(videos);
// return videos; // no need to return
// We need to notify Playlist widget to rebuild itself for second time
notifyListeners(); // mandatory
}
}
When using Provider for state management you don't need to use StatefullWidget, so how can you call a method of the ChangeNotifier on start of the app?
You can simply do that in the constructor of the ChangeNotifier, so that when you point out VideosProvider() to the ChangeNotifierProvider Builder the constructor will get called the first time the provider constructs the VideosProvider, so:
PlayList.dart:
class Playlist extends StatelessWidget {
#override
Widget build(BuildContext context) {
final videosState = Provider.of<VideosProvider>(context);
var videos = videosState.playlist;
return Scaffold(
appBar: AppBar(
title: Text('My Videos'),
),
body: RefreshIndicator(
child: Container(
width: double.infinity,
height: double.infinity,
child: videos.length
? ListView.builder(
itemBuilder: (BuildContext context, index) {
return _videoListItem(context, index, videos, videosState);
},
itemCount: videos.length,
)
: Center(
child: CircularProgressIndicator(),
),
),
onRefresh: () => null,
),
);
}
}
VideosProvider.dart:
class VideosProvider with ChangeNotifier {
VideosProvider(){
fetchVideosList();
}
List _playlist;
int _currentVideoId;
get playlist => _playlist;
void setPlayList(videosList) {
_playlist = videosList;
}
Future fetchVideosList() async {
http.Response response =
await http.get("http://192.168.1.22:3000/videos-list/");
print(json.decode(response.body));
videos = json.decode(response.body)["data"];
setPlayList(videos);
return videos;
}
}
When using a Provider you don’t need to use a StatefulWidget (as of a tutorial by the Flutter team State management
You may use the following tutorial to see how to fetch data with a provider and a
StatelessWidget: Flutter StateManagement with Provider