I am going to combine two stream. But it does not work. What is my mistake ?
My build function is ;
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: Observable.combineLatest2(
getAllDBAccountsBloc.getAllDBAccountsStream,
deleteDBAccountBloc.deleteDBAccountStream,
(accountList, deleteAccountResultModel) {
print("my account list : ${accountList == null}");
return AccountsCombinerResult(
deleteAccountResultBlocModel: deleteAccountResultModel,
accountsList: accountList,
);
},
),
builder: (context, snapshot) {
print("hasData : ${snapshot.hasData}");
if (snapshot.hasData) accountsCombinerResult = snapshot.data;
if (snapshot.hasError) return Text(snapshot.error.toString());
return _buildWidget;
},
);
}
Get All DB Accounts Stream Bloc is
class GetAllDBAccountsBloc {
final _getAllDBAccountsFetcher = PublishSubject<List<AccountDatabaseModel>>();
Observable<List<AccountDatabaseModel>> get getAllDBAccountsStream => _getAllDBAccountsFetcher.stream;
getAllDBAccounts() async {
print("accounts getting");
_getAllDBAccountsFetcher.sink.add(null);
await new Future.delayed(const Duration(seconds: 1));
_getAllDBAccountsFetcher.sink.add(await Repository.getAllDBAccounts());
print("accounts get");
}
dispose() {
_getAllDBAccountsFetcher.close();
}
}
final getAllDBAccountsBloc = GetAllDBAccountsBloc();
Delete DB Account Bloc is
class DeleteDBAccountBloc {
final _deleteDBAccountFetcher = PublishSubject<DeleteAccountResultBlocModel>();
Observable<DeleteAccountResultBlocModel> get deleteDBAccountStream => _deleteDBAccountFetcher.stream;
deleteDBAccount(DeleteAccountRequestBlocModel requestModel) async {
_deleteDBAccountFetcher.sink.add(DeleteAccountResultBlocModel());
await new Future.delayed(const Duration(seconds: 1));
_deleteDBAccountFetcher.sink.add(await Repository.deleteDBAccount(requestModel));
}
dispose() {
_deleteDBAccountFetcher.close();
}
}
final deleteDBAccountBloc = DeleteDBAccountBloc();
Combiner result class is
class AccountsCombinerResult {
final DeleteAccountResultBlocModel deleteAccountResultBlocModel;
final List<AccountDatabaseModel> accountsList;
AccountsCombinerResult({
#required this.accountsList,
#required this.deleteAccountResultBlocModel,
});
}
its mine Run log on android studio..
I/flutter (28323): accounts getting
I/flutter (28323): hasData : false
I/flutter (28323): hasData : false
I/flutter (28323): accounts get
The stream work but i did not get AccountsCombiner Result data.
This build method work but i don't want use it...
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: getAllDBAccountsBloc.getAllDBAccountsStream,
builder: (context, getDbAccountsSnapshot) {
return StreamBuilder(
stream: deleteDBAccountBloc.deleteDBAccountStream,
builder: (context, deleteDbAccountStreamSnapshot) {
if (deleteDbAccountStreamSnapshot.hasData && getDbAccountsSnapshot.hasData) {
print("qweqweq");
accountsCombinerResult = AccountsCombinerResult(
accountsList: getDbAccountsSnapshot.data,
deleteAccountResultBlocModel: deleteDbAccountStreamSnapshot.data,
);
}
if (getDbAccountsSnapshot.hasError) return Text(getDbAccountsSnapshot.error.toString());
if (deleteDbAccountStreamSnapshot.hasError) return Text(deleteDbAccountStreamSnapshot.error.toString());
return _buildWidget;
},
);
},
);
}
You are building a new stream every time the build method is called. You need to keep the stream reference in the state.
StreamController<AccountsCombinerResult> _streamController = StreamController<AccountsCombinerResult>();
#override
void initState() {
super.initState();
_streamController.addStream(Observable.combineLatest2(
getAllDBAccountsBloc.getAllDBAccountsStream,
deleteDBAccountBloc.deleteDBAccountStream,
(accountList, deleteAccountResultModel) {
print("my account list : ${accountList == null}");
return AccountsCombinerResult(
deleteAccountResultBlocModel: deleteAccountResultModel,
accountsList: accountList,
);
},
));
}
#override
void dispose() {
super.dispose();
_streamController.close();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _streamController.stream,
builder: (context, snapshot) {
print("hasData : ${snapshot.hasData}");
if (snapshot.hasData) accountsCombinerResult = snapshot.data;
if (snapshot.hasError) return Text(snapshot.error.toString());
return _buildWidget;
},
);
}
To make this easier you could use the StreamProvider from the provider package.
https://pub.dev/packages/provider
https://pub.dev/documentation/provider/latest/provider/StreamProvider-class.html
It only build the stream once.
#override
Widget build(BuildContext context) {
return StreamProvider<AccountsCombinerResult>(
initialData: null, // not sure if this works, you can try []
create: () => Observable.combineLatest2(
getAllDBAccountsBloc.getAllDBAccountsStream,
deleteDBAccountBloc.deleteDBAccountStream,
(accountList, deleteAccountResultModel) {
print("my account list : ${accountList == null}");
return AccountsCombinerResult(
deleteAccountResultBlocModel: deleteAccountResultModel,
accountsList: accountList,
);
},
),
catchError: (context, error) => AccountsCombinerResult(
deleteAccountResultBlocModel: null,
accountsList: null,
error: error,
),
child: Builder(
builder: (context) {
final data = Provider.of<AccountsCombinerResult>(context);
// maybe null check
if (data.error != null) return Text(data.error.toString());
accountsCombinerResult =data;
return _buildWidget;
},
),
);
}
class AccountsCombinerResult {
final DeleteAccountResultBlocModel deleteAccountResultBlocModel;
final List<AccountDatabaseModel> accountsList;
final dynamic error;
AccountsCombinerResult({
#required this.accountsList,
#required this.deleteAccountResultBlocModel,
this.error,
});
}
The code is not tested so there may be typos or stuff that I missed, but you should get the general idea.
Related
I am trying to popup Get.bottomSheet when starting app.
I did it like bottom code.
#override
void initState() {
_popUpBottomBanner();
}
void _popUpBottomBanner() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
Get.bottomSheet(
...
);
},
);
}
But I want to judge show or hide bottom sheet by API result.
So I changed Code like below.
void _popUpBottomBanner() {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
FutureBuilder<CustomListVO>(
future: Api().getBanners(...),
builder: (BuildContext _, AsyncSnapshot<CustomListVO> snapshot) {
if (snapshot.hasError) return;
if (!snapshot.hasData) return;
if (snapshot.data?.list?.isNotEmpty == true) {
Get.bottomSheet(
...
);
}
return;
},
);
},
);
}
Despite confirming API result arriving properly,
bottomSheet isn't showing.
What is the problem with this code?
Thanks for reading :D
====== resolved like below ========
#override
Widget build(BuildContext context) {
return FutureBuilder<CustomListVO>(
future: Api().getBanners(...),
builder: (BuildContext _, AsyncSnapshot<CustomListVO> snapshot) {
if (snapshot.data?.list?.isNotEmpty == true) {
Get.bottomSheet(
...
);
}
return HomePage();
}
);
}
you need to show bottom sheet that is not required to use Futurebuilder so you can use it like this :
var result = await Api().getBanners(...);
if (result.data?.list?.isNotEmpty == true) {
Get.bottomSheet(
...
);
}
Streambuilder, ChangeNotifier and Consumer cannot figure out how to use correctly. Flutter
I've tried and tried and tried, I've searched a lot but I cannot figure this out:
I'm using a Streambuilder this should update a ChangeNotifier that should trigger rebuild in my Consumer widget. Supposedly...
but even if I call the provider with the (listen: false) option I've got this error
The following assertion was thrown while dispatching notifications for
HealthCheckDataNotifier: setState() or markNeedsBuild() called during
build. the widget which was currently being built when the offending call was made was:
StreamBuilder<List>
Important: I cannot create the stream sooner because I need to collect other informations before reading firebase, see (userMember: userMember)
Widget build(BuildContext context) {
return MultiProvider(
providers: [
/// I have other provider...
ChangeNotifierProvider<HealthCheckDataNotifier>(create: (context) => HealthCheckDataNotifier())
],
child: MaterialApp(...
then my Change notifier look like this
class HealthCheckDataNotifier extends ChangeNotifier {
HealthCheckData healthCheckData = HealthCheckData(
nonCrewMember: false,
dateTime: DateTime.now(),
cleared: false,
);
void upDate(HealthCheckData _healthCheckData) {
healthCheckData = _healthCheckData;
notifyListeners();
}
}
then the Streambuilder
return StreamBuilder<List<HealthCheckData>>(
stream: HeathCheckService(userMember: userMember).healthCheckData,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData) {
if (snapshot.data!.isNotEmpty) {
healthCheckData = snapshot.data?.first;
}
if (healthCheckData != null) {
timeDifference = healthCheckData!.dateTime.difference(DateTime.now()).inHours;
_cleared = healthCheckData!.cleared;
if (timeDifference < -12) {
healthCheckData!.cleared = false;
_cleared = false;
}
///The problem is here but don't know where to put this or how should be done
Provider.of<HealthCheckDataNotifier>(context, listen: false).upDate(healthCheckData!);
}
}
return Builder(builder: (context) {
return Provider<HealthCheckData?>.value(
value: healthCheckData,
builder: (BuildContext context, _) {
return const HealthButton();
},
);
});
} else {
return const Text('checking health'); //Scaffold(body: Loading(message: 'checking...'));
}
});
and finally the Consumer (note: the consumer is on another Route)
return Consumer<HealthCheckDataNotifier>(
builder: (context, hN, _) {
if (hN.healthCheckData.cleared) {
_cleared = true;
return Container(
color: _cleared ? Colors.green : Colors.amber[900],
Hope is enough clear,
Thank you so very much for your time!
it is not possible to setState(or anything that trigger rerender) in the builder callback
just like you don't setState in React render
const A =()=>{
const [state, setState] = useState([])
return (
<div>
{setState([])}
<p>will not work</p>
</div>
)
}
it will not work for obvious reason, render --> setState --> render --> setState --> (infinite loop)
so the solution is similar to how we do it in React, move them to useEffect
(example using firebase onAuthChange)
class _MyAppState extends Stateful<MyApp> {
StreamSubscription<User?>? _userStream;
var _waiting = true;
User? _user;
#override
void initState() {
super.initState();
_userStream = FirebaseAuth.instance.authStateChanges().listen((user) async {
setState(() {
_waiting = false;
_user = user;
});
}, onError: (error) {
setState(() {
_waiting = false;
});
});
}
#override
void dispose() {
super.dispose();
_userStream?.cancel();
}
#override
Widget build(context) {
return Container()
}
}
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.
Say, I have 2 widgets, A and B, where B is nested inside A. Both widgets are wrapped using Consumer. However, only widget A is able to get latest values from the provider, whereas widget B remains as the initial state.
class WidgetA extends StatelessWidget {
Widget build(BuildContext context) {
final FooProvider fooProvider = Provider.of<FooProvider>(context, listen: false);
fooProvider.fetchData();
return Consumer<FooProvider>(
builder: (context, value, child) {
print(value.modelList[0].name); //able to get latest value whenever changes are made to FooProvider.
return GestureDetector(
onTap: () async {
foodProvider.fetchData();
return showDialog(
context: context,
builder: (BuildContext context) {
return WidgetB(); //NOTICE I'm calling WidgetB here
}
)
},
child: WidgetB(); //NOTICE I'm calling WidgetB here
);
}
)
}
}
class WidgetB extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<FooProvider>(
builder: (context, value, child) {
print(value.modelList[0].name); //unable to get latest in showDialog
return Container();
}
)
}
}
EDIT The code for ChangeNotifier:
It's just a regular Provider doing its work.
List<FooModel> modelList = [];
bool isWithinTimeFrame = false;
Future<void> fetchData(email, token, url) async {
await Service(
email,
token,
).fetchCutOff(url).then((response) {
if (response.statusCode == 200) {
var jsonResponse = json.decode(response.body.toString());
bool isSuccess = jsonResponse["success"];
if (isSuccess) {
dynamic formattedResponse = jsonResponse["data"];
List<FooModel> modelList = formattedResponse
.map<FooModel>((json) => FooModel.fromJson(json))
.toList();
setModelList(modelList);
setIsWithinTimeFrame(computeTime(modelList));
} else {}
} else {}
});
}
void setModelList(value) {
modelList = value;
notifyListeners();
}
void setIsWithinTimeFrame(value) {
isWithinTimeFrame = value;
notifyListeners();
}
I want to use await inside streambuilder. However, if you use async inside, you get an error. On the code below !!!!!!!! That's the part I want to solve. Thank you very much if I can tell you how.
class _MemoStreamState extends State<MemoStream> {
final _fireStore = Firestore.instance;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _fireStore
.collection(widget.logInUsrEmail)
.orderBy('id', descending: false)
.snapshots(),
builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
final memos = snapshot.data.documents;
List<MemoMaterial> memoList = [];
for (var memo in memos) {
final memoDocumentID = memo.documentID;
final memoTitle = await PlatformStringCryptor().decrypt(memo.data['title'], _key); !!!!!!!!!!
final memoUsrID = memo.data['usrID'];
final memoUsrPW = memo.data['usrPW'];
final memoText = memo.data['text'];
final memoCreateTime = memo.data['createTime'];
final memoMaterial = MemoMaterial(
logInUsrEmail: widget.logInUsrEmail,
doc: memoDocumentID,
title: memoTitle,
usrID: memoUsrID,
usrPW: memoUsrPW,
text: memoText,
createTime: memoCreateTime,
);
memoList.add(memoMaterial);
}
return Expanded(
child: new ListView.builder(
You should do something like this :
Stream<List<MemoMaterial>> memosStream;
Future<MemoMaterial> generateMemoMaterial(Memo memo) async {
final memoTitle =
await PlatformStringCryptor().decrypt(memo.data['title'], _key);
return MemoMaterial(
logInUsrEmail: widget.logInUsrEmail,
doc: memo.documentID,
title: memoTitle,
usrID: memo.data['usrID'],
usrPW: memo.data['usrPW'],
text: memo.data['text'];,
createTime: memo.data['createTime'],
);
}
#override
void initState() {
memosStream = _fireStore
.collection(widget.logInUsrEmail)
.orderBy('id', descending: false)
.snapshots()
.asyncMap((memos) => Future.wait([for (var memo in memos) generateMemoMaterial(memo)]));
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<List<MemoMaterial>>(
stream: memosStream // Use memostream here
asyncMap() will "transform" every new set of Documents into a list of MemoMaterial, and emit this list into the stream when the action is performed.
Future.wait() allows to perform multiple async requests simultaneously.
You can do it using FutureBuilder inside StreamBuilder in following way.
Stream<List<int>> callme() async* {
yield [1, 2, 3, 4, 5, 6];
}
buildwidget() async {
await Future.delayed(Duration(seconds: 1));
return 1;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: StreamBuilder(
stream: callme(),
builder: (_, sna) {
if (sna.hasData) {
return FutureBuilder(
future: buildwidget(),
builder: (_, snap) {
if (snap.hasData) {
return ListView.builder(
itemCount: sna.data.length,
itemBuilder: (_, index) {
return Text("${sna.data[index]} and ${snap.data}");
},
);
} else {
return CircularProgressIndicator();
}
},
);
} else {
return CircularProgressIndicator();
}
}),
),
);
}
I will prefer to use Getx or Provider State management to Handle the UI if it depends on the async function.
Suppose you want to fetch data from firebase using StreamBuilder() which returns some docs which contains image links then you want to download these images and show from storage. Obviously downloading the image is async type of work. Then you will get error if you show the images with the links you get direct from StreamBuilder().
What you can do is set a variable in getx or provider to show or hide the image Widget. If the Image is being downloaded or not downloaded then set the variable to hide/show the image when the async type of function is completed.