I am creating a class for notifications that is outside of context using bot_toast, which will be called on API errors and such. I need to leave a margin that is relative to the screen width, but I don't have the context here. I could pass the context around, it doesn't seem like the best option.
So I am ending up with something like this
Function CustomToast (message) {
return BotToast.showAttachedWidget(
attachedBuilder: (_) => Align(
alignment: Alignment.topRight,
child: Container(
margin: new EdgeInsets.only(top: 25, right: 25.0),
color: Colors.blue,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
message,
),
),
),
),
duration: Duration(seconds: 20),
target: Offset(520, 520));
}
I need to set the righ margin as a percentage. How would I access the screen width here? is it possible without context? Any suggestion is welcome, even a change of toast library
Perhaps an app level model. Once you have a context, you can set the screen resolution for later retrieval of calculated percentage. I use this approach frequently.
class ScreenInfoViewModel {
List<String> _setupCompleted = [];
String appName;
String packageName;
String version;
String buildNumber;
double screenWidth;
ScreenInfoViewModel() {
init();
}
init() async {
var packageInfo = await PackageInfo.fromPlatform();
appName = packageInfo.appName;
packageName = packageInfo.packageName;
version = packageInfo.version;
buildNumber = packageInfo.buildNumber;
}
bool _smallScreen = false;
bool _mediumScreen = false;
bool _largeScreen = false;
void setScreenSize(double this.screenWidth, double diagonalInches) {
_smallScreen = diagonalInches < 5.11;
_mediumScreen = !_smallScreen && diagonalInches <= 5.6;
_largeScreen = diagonalInches > 5.6;
}
bool get isSmallScreen => _smallScreen;
bool get isMediumScreen => _mediumScreen;
bool get isLargeScreen => _largeScreen;
String get screenSize {
String size = 'S';
if (_mediumScreen) size = 'M';
if (_largeScreen) size = 'L';
return size;
}
}
I use it this way:
class SetupScreenInfo extends HookWidget {
// Use GetIt package to retrieve the model
final ScreenInfoViewModel _s = locator();
final _scaffoldKey = new GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
/// This is the first 'context' with a MediaQuery, therefore,
/// this is the first opportunity to set these values.
/// widthPx and diagonalInches are from sized_context package.
_s.setScreenSize(context.widthPx, context.diagonalInches);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context == null) return;
Navigator.pushReplacementNamed(context, splashRoute);
});
return SafeArea(
child: Scaffold(
key: _scaffoldKey,
body: Material(color: Colors.yellow[300]),
),
);
}
}
Finally, SetupScreenInfo is my initial route from my MaterialApp.
This code is an edit of my production code and has not been tested and is not a runnable example.
Hope this help generate some thought.
Related
first of all, sorry for the lack of information in the title, I just couldn't think of anything better to write.
My problem is that I'm parsing JSON data from an online API and sometimes it displays what I want it to display, and sometimes it doesn't... here is the code:
(The first code block is the class of what I'll be using in the second block, which is the important part and probably where I think the problem is coming from)
import 'dart:convert';
import 'package:http/http.dart' as http;
class CanteenInfo {
final String canteenUrl = 'https://sigarra.up.pt/feup/pt/mob_eme_geral.cantinas';
List canteenMenu = [];
var data;
String workingHours = "yes";
CanteenInfo() {
getWebsiteData();
}
Future getWebsiteData() async {
var response = await http.get(Uri.parse(canteenUrl));
if (response.statusCode == 200) {
data = json.decode(response.body);
workingHours = data[3]["horario"];
print("Hours: " + workingHours);
}
else {
throw Exception('Failed to read $canteenUrl');
}
}
}
class WorkingHours extends StatefulWidget {
const WorkingHours({Key? key}) : super(key: key);
#override
State<WorkingHours> createState() => _WorkingHours();
}
class _WorkingHours extends State<WorkingHours> {
String hours = "yes";
CanteenInfo canteen = CanteenInfo();
void getHours() {
setState(() {
hours = canteen.getHours();
});
}
#override
Widget build(BuildContext context) {
getHours();
return Scaffold(
appBar: AppBar(
title: Text('EasyFood'),
),
body: Center(
child: Container (
margin: const EdgeInsets.only(top: 100),
child: Column(
children: <Widget>[
Text(
'Lunch: ' + hours,
style: const TextStyle(
fontSize: 20
)
),
],
),
),
),
);
}
}
If I haven't made my problem clear, when I run the code, sometimes it displays the text as "Lunch: yes" and sometimes as the correct thing, which is "Lunch: 11:30 às 14:00".
By my understanding, I think what's happening is that sometimes the code can get the API information and time and has time to change the variable before the page loads, and sometimes it doesn't. Either way, I don't know how to fix it so if anyone has any idea I would relly appreciate the help :)
Thanks alot for taking the time to read this
I'm not sure if getHours method exists in CanteenInfo.
But you probably need to override initState method in your stateful widget and call
canteen.getWebsiteData().then((value) {
setState(() => hours = canteen.workingHours);
});
or something like that depending on which method returns data from API
A good decision will be to wait CanteenInfo object "canteen" to be initialized, smth like this:
var flag = false;
loadData()async{
flag = true;
canteen = CanteenInfo();
await getHours();
setState((){flag=false;});
}
Widget build(BuildContext context) {
if (hours == "yes" && !flag)
loadData();
if (flag)
return Scaffold(body:CircularProgressIndicator());
return Scaffold(
appBar: AppBar(
title: Text('EasyFood'),
),
body: Center(
child: Container (
margin: const EdgeInsets.only(top: 100),
child: Column(
children: <Widget>[
Text(
'Lunch: ' + hours,
style: const TextStyle(
fontSize: 20
)
),
],
),
),
),
);
}
I'm not sure if class object initialization works correct here, maybe you also need to add async/await
class CanteenInfo {
final String canteenUrl = 'https://sigarra.up.pt/feup/pt/mob_eme_geral.cantinas';
List canteenMenu = [];
var data;
String workingHours = "yes";
getWebsiteData() async {
var response = await http.get(Uri.parse(canteenUrl));
if (response.statusCode == 200) {
data = json.decode(response.body);
workingHours = data[3]["horario"];
print("Hours: " + workingHours);
}
else {
throw Exception('Failed to read $canteenUrl');
}
}
getHours() {
return workingHours;
}
}
// ------------------------
class WorkingHours extends StatefulWidget {
const WorkingHours({Key? key}) : super(key: key);
#override
State<WorkingHours> createState() => _WorkingHours();
}
// ------------------------
class _WorkingHours extends State<WorkingHours> {
String hours = "yes";
CanteenInfo canteen = CanteenInfo();
#override
void initState() {
super.initState();
canteen.getWebsiteData().then(() {
hours = canteen.getHours();
});
}
}
I have an expansion panel in _buildCategoryListings() that does not expand when the header or the dropdown button is clicked. isExpanded is set to the boolean categoryView.isExpanded. Through printing via the console I can see that the setState is actually updating the bool value but it looks like the actual widget isn't being redrawn perhaps? If I manually set isExpanded to true I see the results I want from the GUI. I also had set isExtended to theExpanded (which is in MovieListingView) which raises the issue of a mutable variable being in a class that extends StatefulWidget, this did give me the desired results though.
The question: How do I get the expansion panel to update the categoryView.isExpanded (via theListings[panelIndex].isExpanded) bool and show it via the GUI?
Thank you in advance.
Side note I thought about using a provider to keep track of this bool but that seems like overkill.
class MovieListingView extends StatefulWidget {
#override
_MovieListingView createState() => _MovieListingView();
MovieListingView(this.movieList);
final MovieCatalog movieList;
//bool theExpanded = false;
List<MovieCategoryView> generateCategoryList() {
List<MovieCategoryView> tempList = [];
List<String> movieCategories = movieList.Categories;
movieCategories.forEach((category) {
MovieCategoryView categoryView = new MovieCategoryView(
movieCategoryName: category.toString(),
movieList: movieList.getMovieCardListByCategory(category));
tempList.add(categoryView);
});
return tempList;
}
}
class _MovieListingView extends State<MovieListingView> {
Widget build(BuildContext context) {
// TODO: implement build
return SingleChildScrollView(
physics: ScrollPhysics(),
padding: EdgeInsets.all(5.0),
child: _buildCategoryListings(),
);
}
List<MovieCategoryView> generateCategoryList() {
List<MovieCategoryView> tempList = [];
List<String> movieCategories = widget.movieList.Categories;
int counter = 0;
movieCategories.forEach((category) {
MovieCategoryView categoryView = new MovieCategoryView(
movieCategoryName: category.toString(),
movieList:
widget.movieList.getMenuItemCardListByCategory(category),
isExpanded: false);
tempList.add(categoryView);
});
return tempList;
}
Widget _buildCategoryListings() {
final List<MovieCategoryView> theListings = generateCategoryList();
return ExpansionPanelList(
expansionCallback: (panelIndex, isExpanded) {
setState(() {
theListings[panelIndex].isExpanded = !isExpanded;
//widget.theExpanded = !isExpanded;
});
},
children: theListings.map((MovieCategoryView movieCategoryView) {
return ExpansionPanel(
canTapOnHeader: true,
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(movieCategoryView.movieCategoryName),
);
},
body: Column(
children: movieCategoryView.movieList,
),
isExpanded: movieCategoryView.isExpanded);
}).toList(),
);
}
}
class MovieCategoryView {
MovieCategoryView(
{#required this.movieCategoryName,
#required this.movieList,
this.isExpanded});
String movieCategoryName;
List<MovieCard> movieList;
bool isExpanded = false;
}
This is happening because whenever the setstate() is called whole widget tree is rebuild and thus when you try changing the isexpandable value ,is gets changed but the
function generateCategoryList(); again gets called and generates the previous list again and again.
Widget _buildCategoryListings() {
final List<MovieCategoryView> theListings = generateCategoryList();
To fix this call the generateCategoryList(); once in initState() and remove the line above line.
I am using Flutter and the Bloc Pattern with rxdart and would like to have a debug mode in my app similar like you enable the developer mode in Android the user should touch 5 times a logo in the UI.
The UI part is pretty simple with:
Column _renderLogo(BuildContext context) => new Column(
children: <Widget>[
GestureDetector(
onTap: () => BlocProvider.of(context).debugEnabledSink.add(true),
child: Container( ...more logo rendering...
With that in mind, I am looking for an easy elegant way to enable the detection of 5 events in 10 seconds. The whole detection should reset when not enough events are detected in any 10 second time window.
You can use a pseudo-timer to achieve this:
const maxDebugTimerSeconds = 10;
const maxTapCount = 5;
DateTime firstTap;
int tapCount;
void doGestureOnTap() {
final now = DateTime.now();
if (firstTap != null && now.difference(firstTap).inSeconds < maxDebugTimerSeconds) {
tapCount++;
if (tapCount >= maxTapCount) {
BlocProvider.of(context).debugEnabledSink.add(true);
}
} else {
firstTap = now;
tapCount = 0;
}
}
...
GestureDetector(
onTap: doGestureOnTap,
...
),
This answer is based on the comment of #pskink above. I just post it here because it seems like a very elegant way of solving my problem. Thank you #pskink
DebugSwitcher might not be the best class Name it's more TimeWindowEventDetector but you get the idea.
class DebugSwitcher {
final Duration window;
final int cnt;
bool value = false;
var timeStamps = [];
DebugSwitcher({this.window = const Duration(seconds: 10), this.cnt = 5});
call(data, sink) {
var now = DateTime.now();
timeStamps.removeWhere((ts) => now.difference(ts) >= window);
timeStamps.add(now);
if (timeStamps.length >= cnt) {
timeStamps = [];
sink.add(value = !value);
}
}
}
Than u listen to the Sink Events this way:
_debugEnabledController.stream
.transform(StreamTransformer.fromHandlers(handleData: DebugSwitcher()))
.listen(
(_) {
_isDebugModeOn.value = true;
infoInternal.value = 'Enabled Debug Mode';
},
);
The Sink is defined like this:
final StreamController<bool> _debugEnabledController =
StreamController<bool>();
Sink<bool> get debugEnabledSink => _debugEnabledController.sink;
In the UI the code looks like this:
Column _renderLogo(BuildContext context) => new Column(
children: <Widget>[
GestureDetector(
onTap: () => BlocProvider.of(context).debugEnabledSink.add(true),
child: Container(
margin: const EdgeInsets.only(top: 10.0, right: 10.0),
height: 80.0,
child: Theme.of(context).primaryColorBrightness ==
Brightness.light
? new Image.asset('assets/app_icon_light.png', fit: BoxFit.contain)
: new Image.asset('assets/app_icon.png', fit: BoxFit.contain),
),
),
],
);
I have a widget A that has a nested widget B. I need to update a field on B (maybe by calling a function of B) when user click on a button in A. How can I do it?
EDIT: more details
Widget B is a widget that I use across my app so I want to leave him as a separate widget. Widget A is composed of widget B plus another button and I want that click on the button will change the the text that inside Widget B
EDIT2 - what I want to achieve:
Widget B is actually a Container that contains TextFormField with some other widgets. I use it for 5 form fields. now I want to add a field for location. the field for location consists of B plus a list that open when user type and there are results from google geocode. When the user clicks on one of the results, I want to set the TextFormField controller text to the value the user type.
So, I don't want to copy all logic and widgets for the text field from B to A.
In my code widget B is FormTextField and A is LocationField
class LocationField extends StatefulWidget {
Location meetingPointData;
LocationField(this.meetingPointData): super();
#override
_LocationFieldFieldState createState() => new _LocationFieldFieldState();
}
class _LocationFieldFieldState extends State<LocationField> {
OverlayEntry _overlayEntry;
var addresses;
final LayerLink _layerLink = LayerLink();
#override
void initState() {
super.initState();
}
onLocationInputChanged(text) {
this.findLocations(text);
}
#override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: this._layerLink,
child: FormTextField('meetingPoint', 'Meeting Point', "", "enter the name of a place or an address", TextInputType.text, onLocationInputChanged, true , {"empty": "Please enter the meeting point"})
);
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
var addressesWidget = <Widget>[];
for(var i=0; i< addresses.length; i++) {
addressesWidget.add( ListTile(
title: Text(addresses[i].addressLine),
onTap: () {
widget.meetingPointData = Location(addresses[i].addressLine);
setState(() {});
this._overlayEntry.remove();
this._overlayEntry = null;
},
),);
}
return OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
link: this._layerLink,
showWhenUnlinked: false,
offset: Offset(10.0, size.height - 45.0),
child: Material(
elevation: 4.0,
child: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: addressesWidget
),
),
),
)
);
}
Future findLocations(text) async {
if(text == "") {
addresses = [];
return;
}
// From a query
final query = text;
try {
addresses = await Geocoder.local.findAddressesFromQuery(query);
var first = addresses.first;
print("${first.featureName} : ${first.coordinates}");
} catch (err) {
}
if(addresses.length > 0) {
if(this._overlayEntry != null) {
this._overlayEntry.remove();
this._overlayEntry = null;
}
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
} else {
}
}
}
Assuming that the widget tree you're thinking of is:
A(
child: B(),
);
Then A should not be able to modify the state of B. Even if it is technically possible through global variables/GlobalKey, it is anti-pattern to do so.
Instead, you should move the state up in the tree and store your field inside A instead of B.
my use case is to create a list view of articles (each item have the same look, there could be huge amount of articles, e.g. > 10000). I tried with
- ListView with ListView.builder: it supposes only to render the item when the item is displayed
- ScrollController: to determine when to load the next items (pagination)
- then I use List to store the data fetched from restful API using http, by adding the data from http to the List instance
this approach is OK, but in case the user keeps on scrolling pages, the List instance will have more and more items, it can crash with stack Overflow error.
If I don't call List.addAll(), instead I assign the data fetched from api, like: list = data;
I have problem that when the user scroll up, he/she won't be able to see the previous items.
Is there a good approach to solve this? Thanks!
import 'package:flutter/material.dart';
import 'package:app/model.dart';
import 'package:app/components/item.dart';
abstract class PostListPage extends StatefulWidget {
final String head;
DealListPage(this.head);
}
abstract class PostListPageState<T extends PostListPage> extends State<PostListPage> {
final int MAX_PAGE = 2;
DealListPageState(String head) {
this.head = head;
}
final ScrollController scrollController = new ScrollController();
void doInitialize() {
page = 0;
try {
list.clear();
fetchNextPage();
}
catch(e) {
print("Error: " + e.toString());
}
}
#override
void initState() {
super.initState();
this.fetchNextPage();
scrollController.addListener(() {
double maxScroll = scrollController.position.maxScrollExtent;
double currentScroll = scrollController.position.pixels;
double delta = 200.0; // or something else..
if ( maxScroll - currentScroll <= delta) {
fetchNextPage();
}
});
}
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
void mergeNewResult(List<PostListItem> result) {
list.addAll(result);
}
Future fetchNextPage() async {
if (!isLoading && mounted) {
page++;
setState(() {
isLoading = true;
});
final List<PostListItem> result = await doFetchData(page);
setState(() {
if (result != null && result.length > 0) {
mergeNewResult(result);
} else {
//TODO show notification
}
isLoading = false;
});
}
}
Future doFetchData(final int page);
String head;
List<PostListItem> list = new List();
var isLoading = false;
int page = 0;
int pageSize = 20;
final int scrollThreshold = 10;
Widget buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isLoading ? 1.0 : 0.0,
child: new CircularProgressIndicator(),
),
),
);
}
#override
Widget build(BuildContext context) {
ListView listView = ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (BuildContext context, int index) {
if (index == list.length) {
return buildProgressIndicator();
}
if (index > 0) {
return Column(
children: [Divider(), PostListItem(list[index])]
);
}
return PostListItem(list[index]);
},
controller: scrollController,
itemCount: list.length
);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(head),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {
},
),
// action button
IconButton(
icon: Icon(Icons.more_horiz),
onPressed: () {
},
),
]
),
body: new RefreshIndicator(
onRefresh: handleRefresh,
child: listView
),
);
}
Future<Null> handleRefresh() async {
doInitialize();
return null;
}
}
in my case, when the list length is 600, I start to get stack overflow error like:
I/flutter ( 8842): Another exception was thrown: Stack Overflow
I/flutter ( 8842): Another exception was thrown: Stack Overflow
screen:
enter image description here
somehow flutter doesn't show any more details of the error.
I wrote some sample code for a related question about paginated scrolling, which you could check out.
I didn't implement cache invalidation there, but it would easily be extendable using something like the following in the getPodcast method to remove all items that are more than 100 indexes away from the current location:
for (key in _cache.keys) {
if (abs(key - index) > 100) {
_cache.remove(key);
}
}
An even more sophisticated implementation could take into consideration the scroll velocity and past user behavior to lay out a probability curve (or a simpler Gaussian curve) to fetch content more intelligently.