Bloc stream how does it get populated - flutter

I have the following home page within my app:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final CategoriesBloc _categoriesBloc = BlocProvider.of<CategoriesBloc>(context);
return Scaffold(
appBar: AppBar(
title: Text('E-Commerce'),
centerTitle: true,
actions: <Widget>[CartButton()],
),
body: StreamBuilder(
stream: _categoriesBloc.outCategories,
builder: (BuildContext context, AsyncSnapshot<List<Category>> categories) {
if (categories.hasData) {
return ListView.builder(
itemCount: categories.data.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(categories.data[index].name,
style: TextStyle(fontSize: 24.0)),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) => BlocProvider<ProductsBloc>(
child: SelectedCategoryPage(),
bloc: ProductsBloc(categories.data[index]),
))));
},
);
}
return SizedBox();
},
));
}}
Which utilizes the categories bloc:
class CategoriesBloc implements BlocBase {
List<Category> _categories;
StreamController<List<Category>> _categoriesController =
StreamController<List<Category>>();
Sink<List<Category>> get _inCategories => _categoriesController.sink;
Stream<List<Category>> get outCategories => _categoriesController.stream;
CategoriesBloc() {
getCategories();
}
void getCategories() {
DbApi dbApi = DbApi();
_categories = dbApi.getCategories();
_inCategories.add(_categories);
}
#override
void dispose() {
_categoriesController.close();
}
}
I'm currently following a Udemy course trying to learn Flutter, my question which the instructor hasn't answered is, you see the following line:
stream: _categoriesBloc.outCategories,
This returns all categories with no issue, but how does it access the data? because from the category bloc I only populate _inCategories with data? I've gone through all the code and I can't find anywhere where outCategories gets populated?
Looking for clarification into how this actually works would be great.

Related

Opening a screen out the result of a statement

enter code hereI want to open a screen to add extra information if it is not set yet. So after the user is logged in I check if the extra info is set. If not I want it to go to a screen to fill in the info. If the user is done it should go to a "Homescreen". If the user info is already set it should immediately go to the home screen.
I already tried to just go to the extra info form and then Navigator.push to the home screen but then it has difficulties with logging out. I searched for a long time but can not find anything.
class CampPage extends StatelessWidget {
final String email;
final String uid;
const CampPage({super.key, required this.email, required this.uid});
#override
Widget build(BuildContext context) {
return FutureBuilder(
// ignore: unrelated_type_equality_checks
future: context.read<UserProvider>().exists(uid) == true
? null
: Future.delayed(Duration.zero, () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NewUserPage(email: email, userId: uid),
),
);
}),
builder: (context, snapshot) => Scaffold(
drawer: const DrawerHomePage(),
appBar: AppBar(
title: const Text("Camp Page"),
),
body: Column(
children: const [
Text("nieuwe features"),
],
),
),
);
}
}
this is one of the things I try but the NewUserPage always pops up and I only want it to pop up if context.read<UserProvider>().exists(uid) == false
also the solution mentioned does not work for me. I think because there is a screen in between the login and logout (The form screen) the logout function does not work properly.
`
class UserPage extends StatelessWidget {
const UserPage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
child: const Text("Submit"),
onPressed: () {
//Log out of Firestore Authentication
},
),
);
}
}
class NewForm extends StatelessWidget {
const NewForm({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
child: const Text("Submit"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const UserPage()),
);
},
),
);
}
}
Widget build(BuildContext context) {
return FutureBuilder(
future: context.read<UserProvider>().exists(uid)
builder: (context, snapshot) {
if (snapshot.hasdata){
if (snapshot.data == true) {
return const UserPage();
} else {
return const NewForm();
}
}
else // show a proggress bar
}
);
}
`
Does someone still have another solution?
I think you should do this:
Widget build(BuildContext context) {
return FutureBuilder(
future: context.read<UserProvider>().exists(uid)
builder: (context, snapshot) {
if (snapshot.hasdata){
if (snapshot.data == true) // then the user exist
else // the user doesn't exist
}
else // show a proggress bar
}
);
}

Automatic update of list data upon change

I have built an app in Flutter that shows a ListView of data pulled from google sheets. What I would like to do is have the list automatically refresh itself when data is changed on google sheets. I'm not sure if it is possible, but any help would be appreciated.
class BodFullList extends StatefulWidget {
#override
_BodFullListState createState() => _BodFullListState();
}
class _BodFullListState extends State<BodFullList> {
final StreamController _streamController = StreamController();
List<DGL> dgl = [];
int index = 0;
#override
void initState() {
super.initState();
getDGL();
}
Future getDGL({int? index}) async {
final dgl = await BodSheetsApi.getAll();
setState(() {
this.dgl = dgl;
});
}
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text(MyApp.title),
centerTitle: true,
),
body:RefreshIndicator(
onRefresh: getDGL,
child: ListView.builder(
itemCount: dgl.length,
itemBuilder: (context, index){
return Card (
child: ListTile(
onTap: () {
Navigator.push(
context, MaterialPageRoute(
builder: (context) => BodEntryEdit(),
settings: RouteSettings(
arguments: dgl[index],
)
)
);
},
title: Text(dgl[index].loc)
),
);
},
),
)
);
}
actually the informations you have provided are not enough. But, as i imagine, you are fetching the data once and not listening. To listen to changes, you need to use websockets. I remember that GoogleSheet allows to use webhooks from there, you can implement whatever you want.

Why my Flutter JSON data didn't updated from setState?

I've made the JSON data and appear it into FutureBuilder with ListView.builder widget. I want to create a favorite Icon in the trailing of the ListView.builder. So i created it with IconButton, but when I create setState to make some item as favorited, the data didn't updated.
Here is my code
import 'package:flutter/material.dart';
import 'package:json_test/class/doa.dart';
import 'package:json_test/page/DoaPage.dart';
class MainPage extends StatefulWidget {
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
Future<List<Doa>> fetchDoa(BuildContext context) async {
final jsonstring =
await DefaultAssetBundle.of(context).loadString('assets/doa.json');
return doaFromJson(jsonstring);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("JSON Data test"),
),
body: Container(
child: FutureBuilder(
future: fetchDoa(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Doa doa = snapshot.data[index];
return Card(
margin: EdgeInsets.all(8),
child: ListTile(
title: Text(doa.judul),
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) =>
DoaPage(
doa: doa,
)));
},
trailing: IconButton(
icon: Icon(
doa.fav
? Icons.favorite
: Icons.favorite_border,
color: doa.fav ? Colors.red : null,
),
onPressed: () =>
setState(() => doa.fav = !doa.fav),
)));
},
);
}
return CircularProgressIndicator();
})));
}
}
and this is the
preview
the thing is that when you call setState you run build again, and that in turn runs the FutureBuilder again with the original Doa object.
you need to keep a variable that will hold the changes in your _MainPageState outside the build method, theres a few ways to do that and in your case its a little more complicated because you need the context in your fetchDoa.
one workaround is creating a doaList variable to hold the fetched data outside the build and changing the fetchDoa function to set the doaList instead of returning it(that's why it's Future now.
but that's not enough because the FutureBuilder will just set the doaList from scrach every time build runs, so we'll add a _isInit bool to check if its the first time running build. after that you should replace all the 'snapshot.data' with doaList as the snapshot holds nothing
class _MainPageState extends State<MainPage> {
List<Doa> doaList;
bool _isInit = true;
Future<void> fetchDoa(BuildContext context) async {
final jsonstring =
await DefaultAssetBundle.of(context).loadString('assets/doa.json');
doaList = doaFromJson(jsonstring);
_isInit = false;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("JSON Data test"),
),
body: Container(
child: FutureBuilder(
future: _isInit? fetchDoa(context): Future(),
builder: (context, _) {
try this and tell me if it works :)

how to trigger search automatically when using SearchDelegate buildSuggestions in flutter

Now I am using SearchDelegate in flutter 2.0.1, this is my buildSuggestions code:
#override
Widget buildSuggestions(BuildContext context) {
var channelRequest = new ChannelRequest(pageNum: 1, pageSize: 10, name: query);
if (query.isEmpty) {
return Container();
}
return FutureBuilder(
future: ChannelAction.fetchSuggestion(channelRequest),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
List<ChannelSuggestion> suggestions = snapshot.data;
return buildSuggestionComponent(suggestions, context);
} else {
return Text("");
}
});
}
Widget buildSuggestionComponent(List<ChannelSuggestion> suggestions, BuildContext context) {
return ListView.builder(
itemCount: suggestions.length,
itemBuilder: (context, index) {
return ListTile(
title: Text('${suggestions[index].name}'),
onTap: () async {
query = '${suggestions[index].name}';
},
);
},
);
}
when select the recommand text, I want to automatically trigger search event(when I click the suggestion text, trigger the search, fetch data from server side and render the result to UI) so I do not need to click search button. this is my search code:
#override
Widget buildResults(BuildContext context) {
var channelRequest = new ChannelRequest(pageNum: 1, pageSize: 10, name: query);
return buildResultImpl(channelRequest);
}
Widget buildResultImpl(ChannelRequest channelRequest) {
return FutureBuilder(
future: ChannelAction.searchChannel(channelRequest),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
List<Channel> channels = snapshot.data;
return buildResultsComponent(channels, context);
} else {
return Text("");
}
return Center(child: CircularProgressIndicator());
});
}
what should I do to implement it? I have tried invoke buildResults function in buildSuggestionComponent but it seems not work.
To update the data based on the query, you can make an API call to get the result when clicking on a suggestion, then use a StreamController to stream the results to the buildResults() method and call showResults().
I'm creating a simple app here for demonstration:
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: Home()));
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
final _controller = StreamController.broadcast();
#override
dispose() {
super.dispose();
_controller.close();
}
Future<void> _showSearch() async {
await showSearch(
context: context,
delegate: TheSearch(context: context, controller: _controller),
query: "any query",
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Search Demo"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: _showSearch,
),
],
),
);
}
}
class TheSearch extends SearchDelegate<String> {
TheSearch({this.context, this.controller});
BuildContext context;
StreamController controller;
final suggestions =
List<String>.generate(10, (index) => 'Suggestion ${index + 1}');
#override
List<Widget> buildActions(BuildContext context) {
return [IconButton(icon: Icon(Icons.clear), onPressed: () => query = "")];
}
#override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {
close(context, null);
},
);
}
#override
Widget buildResults(BuildContext context) {
return StreamBuilder(
stream: controller.stream,
builder: (context, snapshot) {
if (!snapshot.hasData)
return Container(
child: Center(
child: Text('Empty result'),
));
return Column(
children: List<Widget>.generate(
snapshot.data.length,
(index) => ListTile(
onTap: () => close(context, snapshot.data[index]),
title: Text(snapshot.data[index]),
),
),
);
},
);
}
#override
Widget buildSuggestions(BuildContext context) {
final _suggestions = query.isEmpty ? suggestions : [];
return ListView.builder(
itemCount: _suggestions.length,
itemBuilder: (content, index) => ListTile(
onTap: () {
query = _suggestions[index];
// Make your API call to get the result
// Here I'm using a sample result
controller.add(sampleResult);
showResults(context);
},
title: Text(_suggestions[index])),
);
}
}
final List<String> sampleResult =
List<String>.generate(10, (index) => 'Result ${index + 1}');
I have done it through a simple workaround
Simply add this line after your database call
query = query
But be careful of the call looping

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;
}
}
});
},
);
}
}