Why the provider sometimes does not work? - flutter

The provider has a very strange behavior, when a product is added, the isEmpty property changes, but the provider is not called, and when the product is removed, the provider is called, what is the reason for this behavior.
There is a button with a price, when pressed noInCart, the button adds a product and the text on the button changes, if there is a product, then the button has two zones inCart, the left zone deletes the product and the right one adds more, if click on the left, the button changes as needed.
class AllGoodsViewModel extends ChangeNotifier {
var _isEmpty = true;
bool get isEmpty => _isEmpty;
void detailSetting(Goods item) {
final ind = cart.value.indexWhere((element) => element.id == item.id);
if (ind != -1) {
changeButtonState(false);
} else {
changeButtonState(true);
}
}
void changeButtonState(bool state) {
_isEmpty = state;
notifyListeners();
}
}
// adds and reduces a product
void haveItem({required Goods item, required int operation}) async {
final ind = cart.value.indexWhere((element) => element.id == item.id);
if (ind == -1) {
final minCount = item.optState == 0 ? 1 : item.opt!.count;
if (item.count < minCount) {
//order.shake();
} else {
changeButtonState(false); --------- cart is not empty, not working
cart.value.add(item);
final ind = cart.value.length - 1;
cart.value.last.isOpt = item.optState == 0 ? false : true;
cart.value.last.orderCount = minCount;
cart.value = List.from(cart.value);
await SQFliteService.cart.addToCart(cart.value.last);
changeCountInCart(operation);
}
} else {
final count = cart.value[ind].orderCount;
if (count <= item.count) {} else { return; } //order.shake()
if (operation < 0 || count + operation <= item.count) {} else { return; } //order.shake()
changeButtonState(false); --------- cart is not empty, not working
cart.value[ind].orderCount += operation;
cart.value = List.from(cart.value);
await SQFliteService.cart.updateItem(cart.value[ind].id, {"orderCount":cart.value[ind].orderCount});
changeCountInCart(operation);
}
}
class _DetailGoodsPageState extends State<DetailGoodsPage> {
GlobalKey _key = GlobalKey();
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){
Provider.of<AllGoodsViewModel>(context, listen: false).detailSetting(widget.item);
});
}
#override
Widget build(BuildContext context) {
final model = Provider.of<AllGoodsViewModel>(context, listen: false);
Widget inCart(){
return GestureDetector(
onPanDown: (details) {
Goods? item = widget.item;
RenderBox _cardBox = _key.currentContext!.findRenderObject() as RenderBox;
final localPosition = details.localPosition;
final localDx = localPosition.dx;
if (localDx <= _cardBox.size.width/2) {
Goods value = cart.value.firstWhere((element) => element.id == item.id);
if (item.optState == 0 ? value.orderCount <= 1 : value.orderCount <= value.opt!.count) {
setState(() {
final ind = cart.value.indexWhere((element) => element.id == item.id);
if (ind != -1) {
model.changeButtonState(true); ------ cart is empty it works
cart.value[ind].orderCount = 0;
SQFliteService.cart.delete(cart.value[ind].id);
cart.value = List.from(cart.value)..removeAt(ind);
}
});
} else {
model.haveItem(item: item, operation: item.optState == 0 ? -1 : (-1 * value.opt!.count));
}
} else {
model.haveItem(item: item, operation: item.optState == 0 ? 1 : item.count);
}
},
child: ...
);
}
Widget noInCart(){
return Container(
width: size.width - 16.w,
margin: EdgeInsets.symmetric(vertical: 10.h),
key: _key,
child: TextButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Design.appColor),
padding: MaterialStateProperty.all(EdgeInsets.symmetric(vertical: 8.h, horizontal: 10.w)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.h),
))
),
onPressed: (){
Goods? item = widget.item;
model.haveItem(item: item, operation: item.optState == 0 ? 1 : item.count);
},
child: ...
),
);
}
return ScreenUtilInitService().build((context) => Scaffold(
backgroundColor: Colors.white,
body: Container(
height: 64.h,
color: Colors.white,
child: model.isEmpty ? noInCart() : inCart()
)

in order to listen to updates you must have consumers
notifylistners function orders consumers to rebuild with the new data
wrap your widget with a consumer
Consumer<yourproviderclass>(
builder: (context, yourproviderclassinstance, child) => widget,
),

Related

How to play list of video URLs forever or until stopped in flutter? I am stuck :(

I want to create a video player that continuously plays videos until stopped. Thanks to "easeccy" currently I am able to play videos one after another with a buffer, but it stops after playing the last video. I have been trying to loop all these videos...please help :(
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
class VideoPlayerDemo extends StatefulWidget {
const VideoPlayerDemo({Key? key}) : super(key: key);
#override
State<VideoPlayerDemo> createState() => _VideoPlayerDemoState();
}
class _VideoPlayerDemoState extends State<VideoPlayerDemo> {
int index = 0;
double _position = 0;
double _buffer = 0;
bool lock = true;
final Map<String, VideoPlayerController> _controllers = {};
final Map<int, VoidCallback> _listeners = {};
final Set<String> _urls = {
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#1',
'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#2'
};
#override
void initState() {
super.initState();
_playListLoop();
if (_urls.isNotEmpty) {
_initController(0).then((_) {
_playController(0);
});
}
if (_urls.length > 1) {
_initController(1).whenComplete(() => lock = false);
}
}
VoidCallback _listenerSpawner(index) {
return () {
int dur = _controller(index).value.duration.inMilliseconds;
int pos = _controller(index).value.position.inMilliseconds;
int buf = _controller(index).value.buffered.last.end.inMilliseconds;
setState(() {
if (dur <= pos) {
_position = 0;
return;
}
_position = pos / dur;
_buffer = buf / dur;
});
if (dur - pos < 1) {
if (index < _urls.length - 1) {
_nextVideo();
}
}
};
}
VideoPlayerController _controller(int index) {
return _controllers[_urls.elementAt(index)]!;
}
Future<void> _initController(int index) async {
var controller = VideoPlayerController.network(_urls.elementAt(index));
_controllers[_urls.elementAt(index)] = controller;
await controller.initialize();
}
void _removeController(int index) {
_controller(index).dispose();
_controllers.remove(_urls.elementAt(index));
_listeners.remove(index);
}
void _stopController(int index) {
_controller(index).removeListener(_listeners[index]!);
_controller(index).pause();
_controller(index).seekTo(const Duration(milliseconds: 0));
}
void _playController(int index) async {
if (!_listeners.keys.contains(index)) {
_listeners[index] = _listenerSpawner(index);
}
_controller(index).addListener(_listeners[index]!);
await _controller(index).play();
setState(() {});
}
void _previousVideo() {
if (lock || index == 0) {
return;
}
lock = true;
_stopController(index);
if (index + 1 < _urls.length) {
_removeController(index + 1);
}
_playController(--index);
if (index == 0) {
lock = false;
} else {
_initController(index - 1).whenComplete(() => lock = false);
}
}
void _nextVideo() async {
if (lock || index == _urls.length - 1) {
return;
}
lock = true;
_stopController(index);
if (index - 1 >= 0) {
_removeController(index - 1);
}
_playController(++index);
if (index == _urls.length - 1) {
lock = false;
} else {
_initController(index + 1).whenComplete(() => lock = false);
}
}
_playListLoop() async {
if (index + 1 == _urls.length) {
if (_controller(index).value.position ==
_controller(index).value.position) {
setState(() {
_controller(index).initialize();
});
}
}
}
#override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
SizedBox.expand(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: _controller(index).value.size.width,
height: _controller(index).value.size.height,
child: VideoPlayer(
_controller(index)..setVolume(0.0),
),
),
),
),
//Container ends here.
],
);
}
}
Tried while loop and for loop but its repeating the last loaded video only...

Do we have onTapDown and Drag functionality in flutter?

I have a simple usecase which is some how super tricky for a beginner in flutter.
I need these values returned for the scenario explained below
There are 2 containers in a row (green and orange)
OnTapDown on green container it should return ‘Green’ (this is straight forward and done)
Without lifting the finger off the screen, I drag my finger over the Orange container and I need that to return ‘Orange’
How do I solve this?
One solution could be to wrap your layout with GestureDetector and "guess" the position of your elements to then know where the drag ends.
EDIT: Adding a real check on the target position to make it more robust thanks to #GoodSp33d comment:
class DragView extends StatefulWidget {
const DragView({Key? key}) : super(key: key);
#override
_DragViewState createState() => _DragViewState();
}
GlobalKey orangeContainerKey = GlobalKey();
GlobalKey greenContainerKey = GlobalKey();
class _DragViewState extends State<DragView> {
Rect? getGlobalPaintBounds(GlobalKey element) {
final renderObject = element.currentContext!.findRenderObject();
var translation = renderObject?.getTransformTo(null).getTranslation();
if (translation != null && renderObject?.paintBounds != null) {
return renderObject?.paintBounds
.shift(Offset(translation.x, translation.y));
} else {
return null;
}
}
bool isInRect(double x, double y, Rect? rect) {
if (rect != null)
return x >= rect.left &&
x <= rect.right &&
y <= rect.bottom &&
y >= rect.top;
return false;
}
#override
Widget build(BuildContext context) {
double _cursorX = 0;
double _cursorY = 0;
return GestureDetector(
onHorizontalDragUpdate: (details) {
_cursorX = details.globalPosition.dx;
_cursorY = details.globalPosition.dy;
},
onHorizontalDragEnd: (details) {
if (isInRect(
_cursorX, _cursorY, getGlobalPaintBounds(orangeContainerKey)))
print("Orange");
if (isInRect(
_cursorX, _cursorY, getGlobalPaintBounds(greenContainerKey)))
print("Green");
},
child: Row(
children: [
Expanded(
child: Container(key: greenContainerKey, color: Colors.green),
),
Expanded(
child: Container(key: orangeContainerKey, color: Colors.orange),
),
],
),
);
}
}
Second edit moving the detection to the onDragUpdate and checks to make it happens only on rect changes:
GlobalKey? currentObject;
onHorizontalDragUpdate: (details) {
_cursorX = details.globalPosition.dx;
_cursorY = details.globalPosition.dy;
if (isInRect(
_cursorX, _cursorY, getGlobalPaintBounds(orangeContainerKey))) {
if (currentObject == null || currentObject != orangeContainerKey) {
print("Orange");
currentObject = orangeContainerKey;
}
}
if (isInRect(_cursorX, _cursorY,
getGlobalPaintBounds(greenContainerKey))) if (currentObject ==
null ||
currentObject != greenContainerKey) {
print("Green");
currentObject = greenContainerKey;
}
},

Flutter bloc - Event only gets called 2 times inside the bloc logic

I'm having problems to send the event in the bloc. The problem I'm having is that when I send the first event the Bloc is getting called and after I recall it again with a diferrent tab and it works, but when I change to another tab the bloc doesn't get called even if the add event is sending. I have checked with the debbuger and when I call it after the 2 first it doesn't get executed the bloc.
Example where I send the event:
class ListOpportunitiesList extends StatefulWidget {
final String title;
final bool showTopIcons, showFilterMarvTypes;
final OpportunitiesListTypeEnum opportunitiesListTypeEnum;
const ListOpportunitiesList({
Key key,
#required this.title,
#required this.showTopIcons,
#required this.showFilterMarvTypes,
#required this.opportunitiesListTypeEnum,
}) : super(key: key);
#override
_ListOpportunitiesListState createState() => _ListOpportunitiesListState();
}
class _ListOpportunitiesListState extends State<ListOpportunitiesList> {
InvestmentMarvTypeEnum _investmentMarvTypeEnum;
InvestmentStatusEnum _investmentStatusEnum;
InvestmentOpportunityBloc investmentOpportunityBloc;
#override
void initState() {
super.initState();
investmentOpportunityBloc = getIt.getItInstance<InvestmentOpportunityBloc>();
this.getOpportunities();
}
// Calling the event works only the first 2 times
void getOpportunities() {
investmentOpportunityBloc.add(
InvestmentOpportunityLoadEvent(
opportunitiesListTypeEnum: this.widget.opportunitiesListTypeEnum,
investmentMarvTypeEnum: this._investmentMarvTypeEnum,
investmentStatusEnum: this._investmentStatusEnum,
),
);
}
#override
void dispose() {
super.dispose();
this.investmentOpportunityBloc?.close();
}
#override
Widget build(BuildContext context) {
final double height = MediaQuery.of(context).size.height;
final double topMargin = this.widget.showFilterMarvTypes
? height * UIConstants.marginTopPercentageWithFilter
: height * UIConstants.marginTopPercentage;
return BlocProvider<InvestmentOpportunityBloc>(
create: (context) => this.investmentOpportunityBloc,
child: ListView(
padding: EdgeInsets.zero,
children: [
CustomAppBar(
title: '${this.widget.title}',
showLogged: this.widget.showTopIcons,
),
Visibility(
visible: this.widget.opportunitiesListTypeEnum == OpportunitiesListTypeEnum.all,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: InvestmentMarvTypeEnum.values
.map(
(e) => SelectableRoundedRectangleButton(
onPressed: () {
setState(() {
this._investmentMarvTypeEnum =
this._investmentMarvTypeEnum == e ? null : e;
});
this.getOpportunities();
},
selected: e == this._investmentMarvTypeEnum,
text:
'${InvestmentOpportunityUtility.getMarvTypeString(e)}',
),
)
.toList(),
),
),
),
BlocBuilder<InvestmentOpportunityBloc, InvestmentOpportunityState>(
builder: (context, state) {
if (state is InvestmentOpportunityLoaded) {
if (state.investmentOpportunityModel.isNotEmpty) {
return Column(
children: [
for (InvestmentOpportunityModel opportunity
in state.investmentOpportunityModel)
InvestmentOpportunityCard(
investmentOpportunityModel: opportunity,
),
Visibility(
visible: !state.hasReachedMax,
child: ListBottomLoadingWidget(),
),
],
);
} else {
return AppEmptyWidget(
investmentMarvTypeEnum: this._investmentMarvTypeEnum,
topMargin: topMargin,
uiEmptyTypeEnum: UIEmptyTypeEnum.investment_opportunity,
);
}
} else if (state is InvestmentOpportunityError) {
return AppErrorWidget(
topMargin: topMargin,
apiNetworkException: state.apiError,
retryButton: () => this.getOpportunities(),
);
} else {
return AppLoadingWidget(
topMargin: topMargin,
);
}
},
),
],
),
);
}
}
Here is the bloc logic:
class InvestmentOpportunityBloc
extends Bloc<InvestmentOpportunityEvent, InvestmentOpportunityState> {
final GetInvestmentOpportunities getInvestmentOpportunities;
final GetInvestedInvestmentOpportunities getInvestedInvestmentOpportunities;
final GetFavorites getFavorites;
List<InvestmentOpportunityModel> listOpportunity;
InvestmentOpportunityBloc({
#required this.getInvestmentOpportunities,
#required this.getInvestedInvestmentOpportunities,
#required this.getFavorites,
}) : super(InvestmentOpportunityInitial());
#override
Stream<InvestmentOpportunityState> mapEventToState(
InvestmentOpportunityEvent event,
) async* {
final currentState = state;
if (event is InvestmentOpportunityLoadEvent || event is InvestmentOpportunityChangeTab) {
bool modifyType = false;
if (currentState is InvestmentOpportunityLoaded) {
switch ((event as InvestmentOpportunityLoadEvent).opportunitiesListTypeEnum) {
case OpportunitiesListTypeEnum.invested:
{
modifyType = (event as InvestmentOpportunityLoadEvent).investmentStatusEnum !=
currentState
.investmentsOpportunitiesInvestedDTO.investmentStatusEnum;
}
break;
case OpportunitiesListTypeEnum.all:
{
modifyType = (event as InvestmentOpportunityLoadEvent).investmentMarvTypeEnum !=
currentState.investmentOpportunityDTO.investmentMarvTypeEnum;
}
break;
}
if (!modifyType && this._hasReachedMax(currentState)) {
return;
}
}
InvestmentsOpportunitiesInvestedDTO investmentsOpportunitiesInvestedDTO;
InvestmentOpportunityDTO investmentOpportunityDTO;
Either<ApiNetworkException, List<InvestmentOpportunityModel>>
opportunityEither;
switch ((event as InvestmentOpportunityLoadEvent).opportunitiesListTypeEnum) {
case OpportunitiesListTypeEnum.invested:
{
if (currentState is InvestmentOpportunityLoaded && !modifyType) {
currentState.investmentsOpportunitiesInvestedDTO.pageDTO
.incrementPage();
investmentsOpportunitiesInvestedDTO =
currentState.investmentsOpportunitiesInvestedDTO;
} else {
yield InvestmentOpportunityInitial();
investmentsOpportunitiesInvestedDTO =
InvestmentsOpportunitiesInvestedDTO(
pageDTO: PageDTO(),
investmentStatusEnum: (event as InvestmentOpportunityLoadEvent).investmentStatusEnum,
);
}
opportunityEither = await this.getInvestedInvestmentOpportunities(
investmentsOpportunitiesInvestedDTO);
}
break;
case OpportunitiesListTypeEnum.all:
{
if (currentState is InvestmentOpportunityLoaded && !modifyType) {
currentState.investmentOpportunityDTO.pageDTO.incrementPage();
investmentOpportunityDTO = currentState.investmentOpportunityDTO;
} else {
yield InvestmentOpportunityInitial();
investmentOpportunityDTO = InvestmentOpportunityDTO(
pageDTO: PageDTO(),
investmentMarvTypeEnum: (event as InvestmentOpportunityLoadEvent).investmentMarvTypeEnum,
);
}
opportunityEither =
await this.getInvestmentOpportunities(investmentOpportunityDTO);
}
break;
}
yield opportunityEither.fold(
(error) => InvestmentOpportunityError(error),
(opportunities) {
if (currentState is InvestmentOpportunityLoaded &&
opportunities.isEmpty) {
return currentState.copyWith(hasReachedMax: true);
}
this.listOpportunity = currentState is InvestmentOpportunityLoaded && !modifyType
? currentState.investmentOpportunityModel + opportunities
: opportunities;
return InvestmentOpportunityLoaded(
investmentOpportunityModel: this.listOpportunity,
investmentOpportunityDTO: investmentOpportunityDTO,
investmentsOpportunitiesInvestedDTO:
investmentsOpportunitiesInvestedDTO,
hasReachedMax: BlocUtility.hasReachedMax(
opportunities.length,
investmentsOpportunitiesInvestedDTO != null
? investmentsOpportunitiesInvestedDTO.pageDTO.take
: investmentOpportunityDTO.pageDTO.take,
),
);
},
);
} else if (event is InvestmentOpportunityUpdateStatusOfInvestment &&
currentState is InvestmentOpportunityLoaded) {
currentState.investmentOpportunityModel[currentState
.investmentOpportunityModel
.indexOf(event.investmentOpportunityModel)] =
event.investmentOpportunityModel;
yield currentState.copyWith(
investmentOpportunityModel: currentState.investmentOpportunityModel,
);
}
}
bool _hasReachedMax(InvestmentOpportunityState state) {
return state is InvestmentOpportunityLoaded && state.hasReachedMax;
}
}

Flutter StreamBuilder ListView not reloading when stream data changes

I`m trying to build an app that loads an endless feed from a blog in a ListView. At the top, the user has a choice of filtering the feed according to a certain category through the "categorias" menu. When the user taps on the "categorias" menu, another ListView appears with all the available categories. When the user taps on the desired category, the app should return to the feed ListView display only the posts under that category.
Expecter Result:
App call API and retrieves the 10 latest posts
As user scrolls, the next 10 posts are retrieved through successive API calls
User taps on the "categorias" menu and ListView with categories opens.
User taps on the desired category and app returns to the feed Listview, makes an API
call to retrieve the first 10 posts of that category.
As user scrolls, the next 10 posts of that category are retrieved through successive API
calls.
Observed Result:
App call API and retrieves the 10 latest posts
As user scrolls, the next 10 posts are retrieved through successive API calls
User taps on the "categorias" menu and ListView with categories opens.
User taps on the desired category and app returns to the feed Listview, makes an API
call to retrieve the first 10 posts of that category.
Posts of the desired category are appended to the ListView and appear only after the posts
that had been loaded previously.
My question:
How do I have to modify my states or my Bloc, so that I can get the desired result?
Relevant Screenshots
My structure:
PostBloc - My bloc component, which contains the stream definition for Articles and ArticleCategory StreamBuilders. Also contains the methods for making the API calls to
get the articles and article categories.
class PostBloc extends Bloc<PostEvent, PostState> {
final http.Client httpClient;
int _currentPage = 1;
int _limit = 10;
int _totalResults = 0;
int _numberOfPages = 0;
int _categoryId;
bool hasReachedMax = false;
var cachedData = new Map<int, Article>();
PostBloc({#required this.httpClient}) {
//Listen to when user taps a category in the ArticleCategory ListView
_articleCategoryController.stream.listen((articleCategory) {
if (articleCategory.id != null) {
_categoryId = articleCategory.id;
_articlesSubject.add(UnmodifiableListView(null));
_currentPage = 1;
_fetchPosts(_currentPage, _limit, _categoryId)
.then((articles) {
_articlesSubject.add(UnmodifiableListView(articles));
});
_currentPage++;
dispatch(Fetch());
}
});
_currentPage++;
}
List<Article> _articles = <Article>[];
// Category Sink for listening to the tapped category
final _articleCategoryController = StreamController<ArticleCategory>();
Sink<ArticleCategory> get getArticleCategory =>
_articleCategoryController.sink;
//Article subject for populating articles ListView
Stream<UnmodifiableListView<Article>> get articles => _articlesSubject.stream;
final _articlesSubject = BehaviorSubject<UnmodifiableListView<Article>>();
//Categories subjet for the article categories
Stream<UnmodifiableListView<ArticleCategory>> get categories => _categoriesSubject.stream;
final _categoriesSubject = BehaviorSubject<UnmodifiableListView<ArticleCategory>>();
void dispose() {
_articleCategoryController.close();
}
#override
Stream<PostState> transform(
Stream<PostEvent> events,
Stream<PostState> Function(PostEvent event) next,
) {
return super.transform(
(events as Observable<PostEvent>).debounceTime(
Duration(milliseconds: 500),
),
next,
);
}
#override
get initialState => PostUninitialized();
#override
Stream<PostState> mapEventToState(PostEvent event) async* {
//This event is triggered when user taps on categories menu
if (event is ShowCategory) {
_currentPage = 1;
await _fetchCategories(_currentPage, _limit).then((categories) {
_categoriesSubject.add(UnmodifiableListView(categories));
});
yield PostCategories();
}
// This event is triggered when user taps on a category
if(event is FilterCategory){
yield PostLoaded(hasReachedMax: false);
}
// This event is triggered when app loads and when user scrolls to the bottom of articles
if (event is Fetch && !_hasReachedMax(currentState)) {
try {
//First time the articles feed opens
if (currentState is PostUninitialized) {
_currentPage = 1;
await _fetchPosts(_currentPage, _limit).then((articles) {
_articlesSubject.add(UnmodifiableListView(articles)); //Send to stream
});
this.hasReachedMax = false;
yield PostLoaded(hasReachedMax: false);
_currentPage++;
return;
}
//User scrolls to bottom of ListView
if (currentState is PostLoaded) {
await _fetchPosts(_currentPage, _limit, _categoryId)
.then((articles) {
_articlesSubject.add(UnmodifiableListView(articles));//Append to stream
});
_currentPage++;
// Check if last page has been reached or not
if(_currentPage > _numberOfPages){
this.hasReachedMax = true;
}
else{
this.hasReachedMax = false;
}
yield (_currentPage > _numberOfPages)
? (currentState as PostLoaded).copyWith(hasReachedMax: true)
: PostLoaded(
hasReachedMax: false,
);
}
} catch (e) {
print(e.toString());
yield PostError();
}
}
}
bool _hasReachedMax(PostState state) =>
state is PostLoaded && this.hasReachedMax;
Article _getArticle(int index) {
if (cachedData.containsKey(index)) {
Article data = cachedData[index];
return data;
}
throw Exception("Article could not be fetched");
}
/**
* Fetch all articles
*/
Future<List<Article>> _fetchPosts(int startIndex, int limit,
[int categoryId]) async {
String query =
'https://www.batatolandia.de/api/batatolandia/articles?page=$startIndex&limit=$limit';
if (categoryId != null) {
query += '&category_id=$categoryId';
}
final response = await httpClient.get(query);
if (response.statusCode == 200) {
final data = json.decode(response.body);
ArticlePagination res = ArticlePagination.fromJson(data);
_totalResults = res.totalResults;
_numberOfPages = res.numberOfPages;
for (int i = 0; i < res.data.length; i++) {
_articles.add(res.data[i]);
}
return _articles;
} else {
throw Exception('error fetching posts');
}
}
/**
* Fetch article categories
*/
Future<List<ArticleCategory>> _fetchCategories(int startIndex, int limit,
[int categoryId]) async {
String query =
'https://www.batatolandia.de/api/batatolandia/articles/categories?page=$startIndex&limit=$limit';
final response = await httpClient.get(query);
if (response.statusCode == 200) {
final data = json.decode(response.body);
ArticleCategoryPagination res = ArticleCategoryPagination.fromJson(data);
_totalResults = res.totalResults;
_numberOfPages = res.numberOfPages;
List<ArticleCategory> categories = <ArticleCategory>[];
categories.add(ArticleCategory(id: 0 , title: 'Todos', color: '#000000'));
for (int i = 0; i < res.data.length; i++) {
categories.add(res.data[i]);
}
return categories;
} else {
throw Exception('error fetching categories');
}
}
}
Articles - contains a BlocProvider to read the current state set in PostBloc and displays
the corresponding view.
class Articles extends StatelessWidget{
PostBloc _postBloc;
#override
Widget build(BuildContext context) {
return BlocProvider(
builder: (context) =>
PostBloc(httpClient: http.Client())..dispatch(Fetch()),
child: BlocBuilder<PostBloc, PostState>(
builder: (context, state){
_postBloc = BlocProvider.of<PostBloc>(context);
// Displays circular progress indicator while posts are being retrieved
if (state is PostUninitialized) {
return Center(
child: CircularProgressIndicator(),
);
}
// Shows the feed Listview when API responds with the posts data
if (state is PostLoaded) {
return ArticlesList(postBloc:_postBloc );
}
// Shows the Article categories Listview when user clicks on menu
if(state is PostCategories){
return ArticlesCategoriesList(postBloc: _postBloc);
}
//Shows error if there are any problems while fetching posts
if (state is PostError) {
return Center(
child: Text('Failed to fetch posts'),
);
}
return null;
}
)
);
}
}
ArticlesList - Contains a StreamBuilder, which reads the articles data from PostBloc and loads into the feed ListView.
class ArticlesList extends StatelessWidget {
ScrollController _scrollController = new ScrollController();
int currentPage = 1;
int _limit = 10;
int totalResults = 0;
int numberOfPages = 0;
final _scrollThreshold = 200.0;
Completer<void> _refreshCompleter;
PostBloc postBloc;
ArticlesList({Key key, this.postBloc}) : super(key: key);
#override
Widget build(BuildContext context) {
_scrollController.addListener(_onScroll);
_refreshCompleter = Completer<void>();
return Scaffold(
appBar: AppBar(
title: Text("Posts"),
),
body: StreamBuilder<UnmodifiableListView<Article>>(
stream: postBloc.articles,
initialData: UnmodifiableListView<Article>([]),
builder: (context, snapshot) {
if(snapshot.hasData && snapshot != null) {
if(snapshot.data.length > 0){
return Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
ArticlesFilterBar(),
Expanded(
child: RefreshIndicator(
child: ListView.builder(
itemBuilder: (BuildContext context,
int index) {
return index >= snapshot.data.length
? BottomLoader()
: ArticlesListItem(
article: snapshot.data.elementAt(
index));
},
itemCount: postBloc.hasReachedMax
? snapshot.data.length
: snapshot.data.length + 1,
controller: _scrollController,
),
onRefresh: _refreshList,
),
)
],
);
}
else if (snapshot.data.length==0){
return Center(
child: CircularProgressIndicator(),
);
}
}
else{
Text("Error!");
}
return CircularProgressIndicator();
}
)
);
}
#override
void dispose() {
_scrollController.dispose();
}
void _onScroll() {
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
if (maxScroll - currentScroll <= _scrollThreshold) {
postBloc.dispatch(Fetch());
}
}
Future<void> _refreshList() async {
postBloc.dispatch(Fetch());
return null;
}
}
ArticlesCategoriesList - a StreamBuilder, which reads the categories from PostBloc and loads into a ListView.
class ArticlesCategoriesList extends StatelessWidget {
PostBloc postBloc;
ArticlesCategoriesList({Key key, this.postBloc}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Categorias"),
),
body:
SafeArea(
child: StreamBuilder<UnmodifiableListView<ArticleCategory>>(
stream: postBloc.categories,
initialData: UnmodifiableListView<ArticleCategory>([]),
builder: (context, snapshot) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return new Container(
decoration: new BoxDecoration(
color: Colors.white,
),
child: ListTile(
dense: true,
leading: Icon(Icons.fiber_manual_record,color: HexColor(snapshot.data[index].color)),
trailing: Icon(Icons.keyboard_arrow_right),
title: Text(snapshot.data[index].title),
onTap: () {
postBloc.getArticleCategory.add(snapshot.data[index]);
},
));
},
separatorBuilder: (context, index) => Divider(
color: Color(0xff666666),
height: 1,
),
itemCount: snapshot.data.length);
},
)));
}
}
And here I am answering my own question...
In the end, I got everything to run smoothly by clearing the _articles List whenever a category tap event was detected.
So here is the new PostBloc
class PostBloc extends Bloc<PostEvent, PostState> {
final http.Client httpClient;
int _currentPage = 1;
int _limit = 10;
int _totalResults = 0;
int _numberOfPages = 0;
int _categoryId;
bool hasReachedMax = false;
var cachedData = new Map<int, Article>();
List<Article> _articles = <Article>[];
PostBloc({#required this.httpClient}) {
//Listen to when user taps a category in the ArticleCategory ListView
_articleCategoryController.stream.listen((articleCategory) {
if (articleCategory.id != null) {
_categoryId = articleCategory.id;
_currentPage = 1;
_articles.clear();
_fetchPosts(_currentPage, _limit, _categoryId)
.then((articles) {
_articlesSubject.add(UnmodifiableListView(articles));
});
_currentPage++;
dispatch(FilterCategory());
}
});
}
// Category Sink for listening to the tapped category
final _articleCategoryController = StreamController<ArticleCategory>();
Sink<ArticleCategory> get getArticleCategory =>
_articleCategoryController.sink;
//Article subject for populating articles ListView
Stream<UnmodifiableListView<Article>> get articles => _articlesSubject.stream;
final _articlesSubject = BehaviorSubject<UnmodifiableListView<Article>>();
//Categories subjet for the article categories
Stream<UnmodifiableListView<ArticleCategory>> get categories => _categoriesSubject.stream;
final _categoriesSubject = BehaviorSubject<UnmodifiableListView<ArticleCategory>>();
void dispose() {
_articleCategoryController.close();
}
#override
Stream<PostState> transform(
Stream<PostEvent> events,
Stream<PostState> Function(PostEvent event) next,
) {
return super.transform(
(events as Observable<PostEvent>).debounceTime(
Duration(milliseconds: 500),
),
next,
);
}
#override
get initialState => PostUninitialized();
#override
Stream<PostState> mapEventToState(PostEvent event) async* {
//This event is triggered when user taps on categories menu
if (event is ShowCategory) {
_currentPage = 1;
await _fetchCategories(_currentPage, _limit).then((categories) {
_categoriesSubject.add(UnmodifiableListView(categories));
});
yield PostCategories();
}
// This event is triggered when user taps on a category
if(event is FilterCategory){
yield PostLoaded(hasReachedMax: false);
}
// This event is triggered when app loads and when user scrolls to the bottom of articles
if (event is Fetch && !_hasReachedMax(currentState)) {
try {
//First time the articles feed opens
if (currentState is PostUninitialized) {
_currentPage = 1;
await _fetchPosts(_currentPage, _limit).then((articles) {
_articlesSubject.add(UnmodifiableListView(articles)); //Send to stream
});
this.hasReachedMax = false;
yield PostLoaded(hasReachedMax: false);
_currentPage++;
return;
}
//User scrolls to bottom of ListView
if (currentState is PostLoaded) {
await _fetchPosts(_currentPage, _limit, _categoryId)
.then((articles) {
_articlesSubject.add(UnmodifiableListView(_articles));//Append to stream
});
_currentPage++;
// Check if last page has been reached or not
if(_currentPage > _numberOfPages){
this.hasReachedMax = true;
}
else{
this.hasReachedMax = false;
}
yield (_currentPage > _numberOfPages)
? (currentState as PostLoaded).copyWith(hasReachedMax: true)
: PostLoaded(
hasReachedMax: false,
);
}
} catch (e) {
print(e.toString());
yield PostError();
}
}
}
bool _hasReachedMax(PostState state) =>
state is PostLoaded && this.hasReachedMax;
Article _getArticle(int index) {
if (cachedData.containsKey(index)) {
Article data = cachedData[index];
return data;
}
throw Exception("Article could not be fetched");
}
/**
* Fetch all articles
*/
Future<List<Article>> _fetchPosts(int startIndex, int limit,
[int categoryId]) async {
String query =
'https://www.batatolandia.de/api/batatolandia/articles?page=$startIndex&limit=$limit';
if (categoryId != null) {
query += '&category_id=$categoryId';
}
final response = await httpClient.get(query);
if (response.statusCode == 200) {
final data = json.decode(response.body);
ArticlePagination res = ArticlePagination.fromJson(data);
_totalResults = res.totalResults;
_numberOfPages = res.numberOfPages;
List<Article> posts = <Article>[];
for (int i = 0; i < res.data.length; i++) {
_articles.add(res.data[i]);
posts.add(res.data[i]);
}
return posts;
} else {
throw Exception('error fetching posts');
}
}
/**
* Fetch article categories
*/
Future<List<ArticleCategory>> _fetchCategories(int startIndex, int limit,
[int categoryId]) async {
String query =
'https://www.batatolandia.de/api/batatolandia/articles/categories?page=$startIndex&limit=$limit';
final response = await httpClient.get(query);
if (response.statusCode == 200) {
final data = json.decode(response.body);
ArticleCategoryPagination res = ArticleCategoryPagination.fromJson(data);
_totalResults = res.totalResults;
_numberOfPages = res.numberOfPages;
List<ArticleCategory> categories = <ArticleCategory>[];
categories.add(ArticleCategory(id: 0 , title: 'Todos', color: '#000000'));
for (int i = 0; i < res.data.length; i++) {
categories.add(res.data[i]);
}
return categories;
} else {
throw Exception('error fetching categories');
}
}
}

Flutter- detect memory leak

I'm little bit confused because I was thinking there are no memory leak in flutter since there is no concept of weak (if I'm correct).
I'm running this on iOS device.
I'm trying to play videos and initialize some videos beforehand so that user can see it without delay.
To do that I prepared six VideoPlayerController and make those always being initialized while current video is playing.
There are three more initialized VideoPlayerController next to current one and two more initialized ones before current one like image below.
With this logic I play video very smoothly back and forth. But after play about ten videos, app crush because of memory issue.
I tried every function Future, async, await but still eats lots of memories.
I'm not sure but it might be NotificationListener?
Since onNotification returns bool not Future or
is this something to do with main thread or something?
Does anyone know how to fix this memory issue?
Code:
class _SwiperScreenState extends State<SwiperScreen> {
VideoPlayerController _firstController;
VideoPlayerController _secondController;
VideoPlayerController _thirdController;
VideoPlayerController _fourthController;
VideoPlayerController _fifthController;
VideoPlayerController _sixthController;
List<VideoPlayerController> _controllers;
List<String> urls = [
'https://firebasestorage.googleapis.com/v0/b/waitingboy-34497.appspot.com/o/video%2F8-21%2F1534825377992OsfJfKsdf90K8sf?alt=media&token=12245ee4-1598-4f7e-ba28-a9eb72ca474e',
'http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_20mb.mp4',
'https://firebasestorage.googleapis.com/v0/b/waitingboy-34497.appspot.com/o/video%2F8-21%2F1534825377992OsfJfKsdf90K8sf?alt=media&token=12245ee4-1598-4f7e-ba28-a9eb72ca474e',
'http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_20mb.mp4',
];
int currentIndex = 0; //refer to current playing controller index
int videosIndex = 0; //refer to current playing urls index
bool _didGetNotification(ScrollNotification notification) {
if (notification is UserScrollNotification) {
if (notification.direction.toString() == 'ScrollDirection.reverse') {
//swipe to left so add one more video
videosIndex++;
//modify index so that always in the range of 0 ~ 5.
if (currentIndex <= 2) {
final int prepareIndex = currentIndex + 3;
urls.add(
'https://firebasestorage.googleapis.com/v0/b/waitingboy-34497.appspot.com/o/video%2F8-21%2F1534825377992OsfJfKsdf90K8sf?alt=media&token=12245ee4-1598-4f7e-ba28-a9eb72ca474e');
_initVideo(urls[videosIndex], prepareIndex);
} else {
final int prepareIndex = (currentIndex + 3) - 6;
urls.add(
'http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_20mb.mp4');
_initVideo(urls[videosIndex], prepareIndex);
}
}
if (notification.direction.toString() == 'ScrollDirection.forward') {
//swipe to right so back one more video
videosIndex--;
//modify index so that always in the range of 0 ~ 5 .
if (currentIndex >= 2) {
final int videoIndex = videosIndex - 2;
final int prepareIndex = currentIndex - 2;
_initVideo(urls[videoIndex], prepareIndex);
} else {
final int videoIndex = videosIndex - 2;
final int prepareIndex = 4 + currentIndex;
_initVideo(urls[videoIndex], prepareIndex);
}
}
}
return true;
}
Future _initVideo(String url, int initIndex) async {
if (_controllers[initIndex] != null) {
await _controllers[initIndex].dispose();
}
_controllers[initIndex] = new VideoPlayerController.network(url);
await _controllers[initIndex].initialize().then((_) async => await _controllers[initIndex].setLooping(true));
setState(() {});
}
Future _initFirstThree() async {
for (int i = 1; i < urls.length; i++) {
await _initVideo(urls[i], i);
}
}
#override
void initState() {
_controllers = [
_firstController,
_secondController,
_thirdController,
_fourthController,
_fifthController,
_sixthController
];
_initVideo(urls[0], 0).then((_) => _controllers[0].play());
_initFirstThree();
super.initState();
}
#override
void deactivate() {
_controllers[currentIndex].setVolume(0.0);
_controllers[currentIndex].pause();
super.deactivate();
}
#override
void dispose() {
_controllers.forEach((con) {
con.dispose();
});
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Swiper'),
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.disc_full),
onPressed: () {
Navigator
.of(context)
.push(MaterialPageRoute(builder: (context) => Dissmiss()));
},
)
],
),
body: new NotificationListener(
onNotification: _didGetNotification,
child: new Swiper(
itemCount: 6,
itemBuilder: (BuildContext context, int index) {
return _controllers[index].value.initialized
? new AspectRatio(
aspectRatio: _controllers[index].value.aspectRatio,
child: new VideoPlayer(_controllers[index]),
)
: new Center(child: new CircularProgressIndicator());
},
loop: urls.length > 6 ? true : false,
onIndexChanged: (i) async {
currentIndex = i;
final int pauseIndex = i == 0 ? 5 : i - 1;
await _controllers[pauseIndex].pause().then((_) async {
await _controllers[i].play();
});
},
),
),
);
}
}