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; ";
}
}
Related
I'm trying to better understand Futures in Flutter. In this example, my app makes an API call to get some information of type Future<String>. I'd like to display this information in a Text() widget. However, because my String is wrapped in a Future I'm unable to put this information in my Text() widget, and I'm not sure how to handle this without resorting to a FutureBuilder to create the small widget tree.
The following example uses a FutureBuilder and it works fine. Note that I've commented out the following line near the bottom:
Future<String> category = getData();
Is it possible to turn category into a String and simply drop this in my Text() widget?
import 'package:flutter/material.dart';
import 'cocktails.dart';
class CocktailScreen extends StatefulWidget {
const CocktailScreen({super.key});
#override
State<CocktailScreen> createState() => _CocktailScreenState();
}
class _CocktailScreenState extends State<CocktailScreen> {
#override
Widget build(BuildContext context) {
Cocktails cocktails = Cocktails();
Future<String> getData() async {
var data = await cocktails.getCocktailByName('margarita');
String category = data['drinks'][0]['strCategory'];
print('Category: ${data["drinks"][0]["strCategory"]}');
return category;
}
FutureBuilder categoryText = FutureBuilder(
initialData: '',
future: getData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return Text(snapshot.data);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
}
return const CircularProgressIndicator();
},
);
//Future<String> category = getData();
return Center(
child: categoryText,
);
}
}
Here's my Cocktails class:
import 'networking.dart';
const apiKey = '1';
const apiUrl = 'https://www.thecocktaildb.com/api/json/v1/1/search.php';
class Cocktails {
Future<dynamic> getCocktailByName(String cocktailName) async {
NetworkHelper networkHelper =
NetworkHelper('$apiUrl?s=$cocktailName&apikey=$apiKey');
dynamic cocktailData = await networkHelper.getData();
return cocktailData;
}
}
And here's my NetworkHelper class:
import 'package:http/http.dart' as http;
import 'dart:convert';
class NetworkHelper {
NetworkHelper(this.url);
final String url;
Future<dynamic> getData() async {
http.Response response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
String data = response.body;
var decodedData = jsonDecode(data);
return decodedData;
} else {
//print('Error: ${response.statusCode}');
throw 'Sorry, there\'s a problem with the request';
}
}
}
Yes, you can achieve getting Future value and update the state based on in without using Using FutureBuilder, by calling the Future in the initState(), and using the then keyword, to update the state when the Future returns a snapshot.
class StatefuleWidget extends StatefulWidget {
const StatefuleWidget({super.key});
#override
State<StatefuleWidget> createState() => _StatefuleWidgetState();
}
class _StatefuleWidgetState extends State<StatefuleWidget> {
String? text;
Future<String> getData() async {
var data = await cocktails.getCocktailByName('margarita');
String category = data['drinks'][0]['strCategory'];
print('Category: ${data["drinks"][0]["strCategory"]}');
return category;
}
#override
void initState() {
super.initState();
getData().then((value) {
setState(() {
text = value;
});
});
}
#override
Widget build(BuildContext context) {
return Text(text ?? 'Loading');
}
}
here I made the text variable nullable, then in the implementation of the Text() widget I set to it a loading text as default value to be shown until it Future is done0
The best way is using FutureBuilder:
FutureBuilder categoryText = FutureBuilder<String>(
future: getData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text('Loading....');
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
var data = snapshot.data ?? '';
return Text(data);
}
}
},
),
but if you don't want to use FutureBuilder, first define a string variable like below and change your adasd to this :
String category = '';
Future<void> getData() async {
var data = await cocktails.getCocktailByName('margarita');
setState(() {
category = data['drinks'][0]['strCategory'];
});
}
then call it in initState :
#override
void initState() {
super.initState();
getData();
}
and use it like this:
#override
Widget build(BuildContext context) {
return Center(
child: Text(category),
);
}
remember define category and getData and cocktails out of build method not inside it.
I have already tried the solution here. My code already depended on a class passed on a parent class.
import 'package:flutter/cupertino.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../widgets/cupertino_modal_button_row.dart';
import '../widgets/simple_widgets.dart';
import '../models/match.dart';
class AddScoreSection extends StatefulWidget {
AddScoreSection({
this.set,
this.partnerIds,
Key key,
}) : super(key: key);
final MatchSet set;
final List<String> partnerIds;
#override
_AddScoresState createState() => _AddScoresState();
}
class _AddScoresState extends State<AddScoreSection> {
String _partnerText;
#override
Widget build(BuildContext context) {
final set = widget.set;
final _hostId = FirebaseAuth.instance.currentUser.uid;
String _partnerId = set.hostTeam != null
? set.hostTeam.firstWhere(
(element) => element != FirebaseAuth.instance.currentUser.uid)
: null;
Future<String> _partnerName(String partnerId) async {
if (partnerId == null) {
return null;
}
final userData = await FirebaseFirestore.instance
.collection('users')
.doc(partnerId)
.get();
return userData['name']['full_name'];
}
print(widget.set.visitingGames);
return CupertinoFormSection(
header: const Text('Set'),
children: [
CupertinoModalButtonRow(
builder: (context) {
return CupertinoActionSheet(
title: _partnerText == null
? const Text('Select a Partner')
: _partnerText,
actions: widget.partnerIds
.map((partner) => CupertinoActionSheetAction(
child: FutureBuilder<String>(
future: _partnerName(partner),
builder: (ctx, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return SimpleWidget.loading10;
}
return Text(snapshot.data);
},
),
onPressed: () async {
final partnerTemp = await _partnerName(partner);
_partnerId = partner;
setState(() {
_partnerText = partnerTemp;
});
set.addToHostTeam = [_partnerId, _hostId];
set.addToVisitTeam = widget.partnerIds
.where((element) =>
widget.set.hostTeam.contains(element))
.toList();
print(set.hostTeam);
Navigator.of(context).pop();
},
))
.toList());
},
buttonChild:
_partnerText == null ? 'Select your Partner' : _partnerText,
prefix: 'Your Partner'),
ScoreEntryRow(
setsData: widget.set,
prefix: 'Team Score',
),
ScoreEntryRow(
prefix: 'Opponent Score',
setsData: widget.set,
isHostMode: false,
),
],
);
}
}
class ScoreEntryRow extends StatefulWidget {
ScoreEntryRow({
Key key,
#required this.setsData,
#required this.prefix,
this.isHostMode: true,
}) : super(key: key);
final MatchSet setsData;
final String prefix;
final bool isHostMode;
#override
_ScoreEntryRowState createState() => _ScoreEntryRowState();
}
class _ScoreEntryRowState extends State<ScoreEntryRow> {
#override
Widget build(BuildContext context) {
final ValueNotifier _games = widget.isHostMode
? ValueNotifier<int>(widget.setsData.hostGames)
: ValueNotifier<int>(widget.setsData.visitingGames);
List<int> scoreList = List.generate(9, (index) => index + 1);
return CupertinoModalButtonRow(
builder: (context) {
return SizedBox(
height: 300,
child: ListView.builder(
itemCount: scoreList.length,
itemBuilder: (context, i) {
return CupertinoButton(
child: Text(scoreList[i].toString()),
onPressed: () {
setState(() {
if (widget.isHostMode) {
widget.setsData.setHostGames = scoreList[i];
widget.setsData.setVisitGames = 9 - scoreList[i];
return;
}
widget.setsData.setVisitGames = scoreList[i];
widget.setsData.setHostGames = 9 - scoreList[i];
});
});
}),
);
},
buttonChild: widget.isHostMode
? widget.setsData.hostGames == null
? '0'
: widget.setsData.hostGames.toString()
: widget.setsData.visitingGames == null
? '0'
: widget.setsData.visitingGames.toString(),
prefix: widget.prefix,
);
}
}
The code for the custom class is:
class MatchSet {
MatchSet(this.setData);
final Map<String, dynamic> setData;
List<String> hostTeam;
List<String> visitingTeam;
int hostGames;
int visitingGames;
set addToHostTeam(List<String> value) {
if (setData.values.every((element) => element != null))
hostTeam = setData['host_team'];
hostTeam = value;
}
set addToVisitTeam(List<String> value) {
if (setData.values.every((element) => element != null))
visitingTeam = setData['visiting_team'];
visitingTeam = value;
}
set setHostGames(int value) {
if (setData.values.every((element) => element != null))
hostGames = setData['host_games'];
hostGames = value;
}
set setVisitGames(int value) {
if (setData.values.every((element) => element != null))
visitingGames = setData['visiting_games'];
visitingGames = value;
}
Map<List<String>, int> get matchResults {
return {
hostTeam: hostGames,
visitingTeam: visitingGames,
};
}
List<String> get winningTeam {
if (matchResults.values.every((element) => element != null)) {
return null;
}
return matchResults[hostTeam] > matchResults[visitingTeam]
? hostTeam
: visitingTeam;
}
String result(String userId) {
if (matchResults.values.every((element) => element != null)) {
return null;
}
if (winningTeam.contains(userId)) {
return 'W';
}
return 'L';
}
bool get isUpdated {
return hostGames != null && visitingGames != null;
}
}
The state does not change for the sibling as you can see in the video. I have also trying the whole button in a ValueListenableBuilder, nothing changes.
What am I doing wrong? By all my knowledge the sibling row widget should rebuild, but it is not rebuilding.
You can use multiple approaches
the solution that you point out is a callback function and it works fine ... you use a function that when it is called it would go back to its parent widget and do some stuff there(in your case setState)
but It does not seem that you are using this it.
the other one is using StateManagement. you can use GetX , Provider, RiverPod,... .
in this one, you can only rebuild the widget that you want but in the first solution, you have to rebuild the whole parent widget.
both solutions are good the second one is more professional and more efficient but i think the first one is a must-know too
I have two pages. one is Route, the second is Stops. Also, my code contains an algorithm that is sorted stops by the routes. When I did the test example and pass the stops on the same page as routes, so everything works fine, but for better UI I want to put arguments in the constructor and in onTap method. How can I pass arguments from this algorithm and terms from another screen into another screen?
the first screen:
body: FutureBuilder(
future: getMarshrutWithStops(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
List<RouteWithStops> routes = snapshot.data;
print(routes?.toString());
return (routes == null)
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StopsPage(
stId: routes[index].stop[index].stId
)));
},
//the algorithm which is sorted everything by id's
Future<List<RouteWithStops>> getMarshrutWithStops() async {
List<Routes> routes = [];
List<ScheduleVariants> variants = [];
List<StopList> stops = [];
final TransportService transService = TransportService();
routes.addAll((await transService.fetchroutes()).toList());
stops.addAll(await transService.fetchStops());
variants.addAll(await transService.fetchSchedule());
List<RouteWithStops> routesWithStops = [];
for (Routes route in routes) {
final routeWithStops = RouteWithStops();
routesWithStops.add(routeWithStops);
routeWithStops.route = route;
routeWithStops.variant =
variants.where((variant) => variant.mrId == route.mrId).first;
List<RaceCard> cards = [];
cards.addAll(
await transService.fetchRaceCard(routeWithStops.variant.mvId));
print(cards);
List<StopList> currentRouteStops = [];
cards.forEach((card) {
stops.forEach((stop) {
if (card.stId == stop.stId) {
currentRouteStops.add(stop);
}
});
});
routeWithStops.stop = currentRouteStops;
}
return routesWithStops;
}
The second page where I want all sorted stops be stored:
class StopsPage extends StatelessWidget {
final int stId;
const StopsPage({Key key, this.stId}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: FutureBuilder(
future: getMarshrutWithStops(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
List<RouteWithStops> routes = snapshot.data;
print(routes?.toString());
return (routes == null)
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: routes.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(routes[index].stop.toString()),
);
});
},
),
);
}
Future<List<RouteWithStops>> getMarshrutWithStops() async {
List<Routes> routes = [];
List<ScheduleVariants> variants = [];
List<StopList> stops = [];
final TransportService transService = TransportService();
routes.addAll((await transService.fetchroutes()).take(10).toList());
stops.addAll(await transService.fetchStops());
variants.addAll(await transService.fetchSchedule());
List<RouteWithStops> routesWithStops = [];
for (Routes route in routes) {
final routeWithStops = RouteWithStops();
routesWithStops.add(routeWithStops);
routeWithStops.route = route;
routeWithStops.variant =
variants.where((variant) => variant.mrId == route.mrId).first;
List<RaceCard> cards = [];
cards.addAll(
await transService.fetchRaceCard(routeWithStops.variant.mvId));
print(cards);
List<StopList> currentRouteStops = [];
cards.forEach((card) {
stops.forEach((stop) {
if (card.stId == stop.stId) {
currentRouteStops.add(stop);
}
});
});
routeWithStops.stop = currentRouteStops;
}
return routesWithStops;
}
}
I just thought that I didn’t need to copy and paste the entire algorithm on all pages, maybe I only need a part of the algorithm that starts with a for-loop and transfer it to the second page, where all the filtered stops should be. I can't figure out what to put in the onTap function and what to pass to the constructor on the Stops page.
you can do something like this
In onTap of First Page pass the value as name parameter
onTap: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => StopPage(stId: routes[index].stop[index].stId))
);
}
Excess the same on StopPage, by using the constructor
class StopPage extends StatefulWidget {
final dynamic stId;
StopPage({this.stId});
}
Hello I'm new to flutter, and I'm trying to do a simple user profile screen for a user who logged in but I stumbled in some errors, the first one was for when I tried to use the StreamBuilder() where the stream didn't get any data from the getter in the UserProvider()(that's where I putted my BL) it kept saying getCurrentUserData() was called on null, so i just connected it directly to the UserService() and it worked, but then when I tried to edit the user info and have the TextFormField() be filled with the user data, via the initState() and have the fullNameController get the data from the UserModel() the error returned it keeps saying fullName was called on null! how do I resolve this can anyone point on where I'm going wrong about here?
P.S I'm following this tutorial to build this.
My StreamBuilder() connected to UserProvider:
return StreamBuilder<UserModel>(
stream: userProviderData.getCurrentUserData(),
builder: (context, snapshot) {})
My StreamBuilder() directly connected to UserService:
Directly connected to UserService
return StreamBuilder<UserModel>(
stream: userService.getCurrentUser(),
builder: (context, snapshot) {})
UserService() class:
// Get User
Stream<UserModel> getCurrentUser() {
return _db.collection('users').doc(_auth.currentUser.uid).snapshots().map(
(user) => UserModel.fromJson(user.data()),
);
}
// Add or Update User info
Future<void> saveUser(UserModel user) {
final _options = SetOptions(merge: true);
return _db.collection('users').doc(user.userId).set(user.toMap(), _options);
}
UserProvider() class:
final userProvider = ChangeNotifierProvider<UserProvider>((ref) {
return;
});
class UserProvider with ChangeNotifier {
final userService = UserService();
String _userId;
String _fullName;
// Getters
String get userId => _userId;
String get fullName => _fullName;
Stream<UserModel> get getCurrentUserData => userService.getCurrentUser();
// Setters
set changeFullName(String fullName) {
_fullName = fullName;
notifyListeners();
}
// Functions
void loadUser(UserModel userModel) {
_userId = userModel.userId;
_fullName = userModel.fullName;
}
void updateUser() {
final _currentUser = UserModel(
userId: _userId,
fullName: _fullName,
);
userService.saveUser(_currentUser);
}
}
EditProfileScreen():
class EditProfileScreen extends StatefulWidget {
const EditProfileScreen({this.userModel});
final UserModel userModel;
#override
_EditProfileScreenState createState() => _EditProfileScreenState();
}
class _EditProfileScreenState extends State<EditProfileScreen> {
final _fullNameController = TextEditingController();
final _validator = Validator();
#override
void dispose() {
_fullNameController.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
final userStream = context.read(userProvider);
if (widget.userModel != null) {
// Edit
_fullNameController.text = widget.userModel.fullName;
userStream.loadUser(widget.userModel);
}
}
#override
Widget build(BuildContext context) {
final userData = context.read(userProvider);
return Scaffold(
body: Column(
children: [
TextFormFiled(
hintText: ‘Full name’,
keyboardType: TextInputType.name,
controller: _fullNameController,
validator: (value) => _validator.validateFullName(
value,
),
onChanged: (value) {
userData.changeFullName = value;
debugPrint(value);
}
),
ElevatedButton(
onPressed: () {
userData.updateUser();
Navigator.of(context).pop();
},
child: const Text(‘Save’),
),
],
),
);
}
}
Did you forget to return something?
final userProvider = ChangeNotifierProvider<UserProvider>((ref) {
return; //return a UserProvider()
});
I'm fetching this API https://rickandmortyapi.com/api/character and putting the data inside a Stream so I can infinite scroll over a Gridview of Cards with every character.
Fetching the first page with a FutureBuilder it works, but trying to use a StreamBuilder just doesn't update anything as if it wasn't receiving any data.
Here's the the Provider.dart
class CharacterProvider {
final _url = 'rickandmortyapi.com';
final _characterStream = StreamController<List<Character>>.broadcast();
List<Character> _characters = [];
int currentPage = 1;
Function(List<Character>) get characterSink => _characterStream.sink.add;
Stream<List<Character>> get characterStream => _characterStream.stream;
void dispose() {
_characterStream?.close();
}
Future<Map<String, dynamic>> fetchData(
String path, Map<String, dynamic> header) async {
print(header);
final response = await http.get(
Uri.https(_url, 'api/$path', header),
);
if (response.statusCode == 200) {
final results = jsonDecode(response.body);
return results;
} else {
throw Exception('Fallo al cargar personajes');
}
}
Future<List<Character>> fetchCharacters() async {
final path = 'character';
final header = {
'page': currentPage.toString(),
};
final data = await fetchData(path, header);
final characterFetched = Characters.fromJsonList(data['results']);
_characters.addAll(characterFetched.character);
characterSink(_characters);
if (currentPage < data['info']['pages']) {
currentPage++;
}
return characterFetched.character;
}
}
The stream of StreamBuilder in the widget is subscribed to characterStream but it is always on null.
class _CharacterCardsState extends State<CharacterCards> {
final _scrollController = ScrollController();
Future<List<Character>> _characters;
int cards;
bool loading;
#override
void initState() {
super.initState();
print('Cards: init');
_characters = initFetch();
loading = true;
cards = 6;
_scrollController.addListener(updateCards);
}
Future<List<Character>> initFetch() async {
final fetch = await CharacterProvider().fetchCharacters();
return fetch;
}
#override
Widget build(BuildContext context) {
CharacterProvider().fetchCharacters();
print('Cards: build');
return GridView.builder(
itemCount: cards,
controller: _scrollController,
itemBuilder: (context, index) {
return StreamBuilder(
stream: CharacterProvider().characterStream,
builder: (BuildContext context,
AsyncSnapshot<List<Character>> snapshot) {
if (snapshot.hasData) {
loading = false;
final character = snapshot.data;
return GestureDetector(
onTap: () {
cardView(context, character, index);
},
child: ofCard(character, index),
);
} else {
return ofLoading(widget.size);
}
},
);
});
}
On debug, the values added to the sink are non-null. The data is fetching correctly but the sink.add() doesn't seem to be working.
I believe you're trying to use provider package (that's why you named your class CharacterProvider() I think), either way the problem is you're not saving a reference of that class, you're creating them anew each time you call CharacterProvider().someMethod so the initFetch CharacterProvider().fetchCharacters() and the stream CharacterProvider().characterStream are not related
Just like your scrollController you should create a final characterProvider = CharacterProvider() and call it in all your methods that requires it
PS: don't call a future CharacterProvider().fetchCharacters(); inside build like that, it's an antipattern
Try this.
class _CharacterCardsState extends State<CharacterCards> {
final _scrollController = ScrollController();
Future<List<Character>> _characters;
int cards;
bool loading;
#override
void initState() {
super.initState();
_characters = CharacterProvider();
_characters.fetchCharacters();
loading = true;
cards = 6;
_scrollController.addListener(updateCards);
}
#override
void dispose(){
_characters.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return GridView.builder(
itemCount: cards,
controller: _scrollController,
itemBuilder: (context, index) {
return StreamBuilder(
stream: _characters.characterStream,
builder: (BuildContext context,
AsyncSnapshot<List<Character>> snapshot) {
if (snapshot.hasData) {
setState(()=>loading=false);
final character = snapshot.data;
return GestureDetector(
onTap: () {
cardView(context, character, index);
},
child: ofCard(character, index),
);
} else {
return ofLoading(widget.size);
}
},
);
});
}
I don't know why you are putting streambuilder inside gridview but logically above code should work.