Flutter Method to Display opening hours using optimized code - flutter

I have managed to compile the code below but know that I can display the opening hours using a better method. I believe it can be done using Future Builder but I am not experienced as yet. The code below loads the items and then goes and find out the opening hours. Can anyone assist to optimize this code so It shows on first instance. I believe there are performance issues with the code below.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart';
import 'package:rapideats/global/global.dart';
import '../mainScreens/menus_screen.dart';
import '../models/sellers.dart';
class SellersDesignWidget extends StatefulWidget {
Sellers? model;
BuildContext? context;
SellersDesignWidget({this.model,this.context});
String status = "";
#override
State<SellersDesignWidget> createState() => _SellersDesignWidgetState();
}
class _SellersDesignWidgetState extends State<SellersDesignWidget> {
var status = "Closed";
getOpeningHours() async
{
var date = DateTime.now();
var from = "";
var to = "";
var TodaysDate = DateFormat('EEEE').format(date).substring(0,3).toLowerCase();
// prints mon,tue etc.
QuerySnapshot oh = await FirebaseFirestore.instance.collection("openingHours")
.where("sellerUID", isEqualTo:widget.model!.sellerUID!)
.get();
if(oh.docs.isNotEmpty)
{
for (int i=0; i < oh.docs.length; i++)
{
from = oh.docs[i][TodaysDate+"From"].toString();
to = oh.docs[i][TodaysDate+"To"].toString();
}
if(from == "00:00" && to == "00:00")
{
setState(() {
status = "Closed";
} );
}else
{
setState(() {
status = "Open Now: " + TodaysDate.toTitleCase() + " " + from + " - " + to + "Hrs";
} );
}
}
}
void initState(){
super.initState();
getOpeningHours();
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child : Padding(
padding: const EdgeInsets.all(6.0),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(widget.model!.sellerAvatarUrl!,
height: 150,
width: 150,
fit:BoxFit.cover,
),
),
const SizedBox(width: 10.0,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 20,
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
widget.model!.sellerName!.toTitleCase(),
style: const TextStyle(
color:Colors.black,
fontSize: 24,
fontFamily:"Acme",
),
),
),
const SizedBox(
width: 10,
),
],
),
const SizedBox(
height: 20,
),
Row(
children: [
Text(
status,
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
],
),
const SizedBox(
height: 20,
),
Row(
children: [
Padding(
padding: const EdgeInsets.all(4.0),
child:
status == "Closed"
? const Text(
"",
)
: ElevatedButton(
child: const Text("Order Now"),
style: ElevatedButton.styleFrom(
backgroundColor:Colors.blueAccent,
),
onPressed: ()
{
Navigator.push(context, MaterialPageRoute(builder:(c)=> MenusScreen(model:widget.model)));
},
)
),
]
),
],
),
),
],
),
),
);
}
}

I recommend using a state management package to handle your UI states in an easier and cleaner way.
As an example you can use Bloc or Provider (There's more packages)
But as an answer for your question, and to handle your state using FutureBuilder to optimize your UI Performance you can use this code
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:intl/intl.dart';
import 'package:rapideats/global/global.dart';
import '../mainScreens/menus_screen.dart';
import '../models/sellers.dart';
/// We Changed it to StatelessWidget as we don't need to rebuild the whole screen again
class SellersDesignWidget extends StatelessWidget {
final Sellers? model;
SellersDesignWidget({this.model});
/// We will use the same function with some modification to get the data and return the value to the FutureBuilder
Future<String> getOpeningHours() async
{
String status = "Closed";
DateTime date = DateTime.now();
String from = "";
String to = "";
String todaysDate = DateFormat('EEEE').format(date).substring(0, 3).toLowerCase();
// prints mon,tue etc.
QuerySnapshot oh = await FirebaseFirestore.instance.collection("openingHours")
.where("sellerUID", isEqualTo: model!.sellerUID!)
.get();
if (oh.docs.isNotEmpty) {
for (int i = 0; i < oh.docs.length; i++) {
from = oh.docs[i][todaysDate + "From"].toString();
to = oh.docs[i][todaysDate + "To"].toString();
}
if (from == "00:00" && to == "00:00") {
status = "Closed";
} else {
status = "Open Now: " + todaysDate.toTitleCase() + " " + from + " - " + to + "Hrs";
}
}
return status;
}
#override
Widget build(BuildContext context) {
/// As most of your UI doesn't depend on the data from Firebase so it can be shown directly
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(6.0),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image.network(model!.sellerAvatarUrl!,
height: 150,
width: 150,
fit: BoxFit.cover,
),
),
const SizedBox(width: 10.0,),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 20,
),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Text(
model!.sellerName!.toTitleCase(),
style: const TextStyle(
color: Colors.black,
fontSize: 24,
fontFamily: "Acme",
),
),
),
const SizedBox(
width: 10,
),
],
),
const SizedBox(
height: 20,
),
/// For the part needs the remote data we wrap it in future builder
FutureBuilder(
future: getOpeningHours(),
builder: (ctx, AsyncSnapshot<String?> status) {
/// When data is loading(status == null) we display a progress indicator and prevent the user from performing any actions
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
status.data == null ?
//Change SizedBox Size if needed
SizedBox(
width: 30, child: CircularProgressIndicator(),)
: Text(
status.data!,
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
],
),
const SizedBox(
height: 20,
),
Row(
children: [
Padding(
padding: const EdgeInsets.all(4.0),
child:
status.data == "Closed"
? const Text(
"",
)
: ElevatedButton(
child: status.data == null ?
//Change SizedBox Size if needed
SizedBox(
width: 30, child: CircularProgressIndicator(),) : Text("Order Now"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
),
onPressed: () {
if (status.data != null) {
Navigator.push(context,
MaterialPageRoute(builder: (c) => MenusScreen(model: model)));
}
},
)
),
]
),
],
);
}),
],
),
),
],
),
),
);
}
}

The cleanest way is: 1 - You show the first paint and give a condition to the opening hours block. If hours have been fetched, display them, else, show a progress indicator or a loading text or any kind of indication to the user that something is being loaded.
2- Then, in the init state, don't forget to set haveFetched to false when you call the function, and to true when it finishes, you call the fetch function. You need to handle possible errors as well by giving it some sort of default times or 'error' text. As Hesham Erfan mentioned, if you have a UI heavy design, you should use a state management solution and stateless widgets since it will improve performance. I recommend:1 - getx for small projects (fast to code, also includes navigator, context-less toasts, snackbars, screen width and height and more!), 2 - provider all around good state manager (very popular good docs, also easy to learn), and 3 - bloc for corporate (it has a lot of boiler plate but a very similar structure all around). You would have to move the init state logic to the state managing solution you implement.

Related

Flutter: image messing up Flexible/Expanded sizes

Im trying to make something in flutter that looks like a twitter clone. The Tweet, called a wave, can be seen in the following:
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:timeago/timeago.dart' as timeago;
import '../../../../../../blocs/vote/vote_bloc.dart' as vote;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:like_button/like_button.dart';
import '../../../../../../blocs/profile/profile_bloc.dart';
import '../../../../../../blocs/wave_liking/wave_liking_bloc.dart';
import '../../../../../../models/user_model.dart';
import '../../../../../../models/wave_model.dart';
import '../../../../../../widgets/text_splitter.dart';
import '../../generic_view.dart';
import '../../photo_view/photo_view.dart';
class WaveTile extends StatelessWidget {
const WaveTile({
Key? key,
required this.poster,
required this.wave,
this.extendBelow = false,
}) : super(key: key);
final User poster;
final Wave wave;
final bool extendBelow;
#override
Widget build(BuildContext context) {
User user =
(BlocProvider.of<ProfileBloc>(context).state as ProfileLoaded).user;
return IntrinsicHeight(
child: Row(
children: [
waveColumn(context),
Expanded(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
waveHeader(poster, wave, context, user),
waveText(wave, context),
if (wave.imageUrl != null) waveImage(wave, context),
waveButtons(wave),
],
))
],
),
);
}
Expanded waveColumn(BuildContext context) {
return Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 40,
height: 40,
child: InkWell(
child: Hero(
tag: 'wave${wave.id}',
child: CircleAvatar(
backgroundImage:
CachedNetworkImageProvider(poster.imageUrls[0])),
),
onTap: () {
BlocProvider.of<vote.VoteBloc>(context)
.add(vote.LoadUserEvent(user: poster));
Navigator.pushNamed(
context,
'/votes',
);
},
),
),
if (extendBelow)
Expanded(
child: VerticalDivider(
color: Color.fromARGB(255, 207, 207, 207),
thickness: 2,
width: 10,
),
),
//add a grey line
]),
);
}
}
Widget waveHeader(User poster, Wave wave, BuildContext context, User user) {
return Row(
children: [
Container(
margin: const EdgeInsets.only(right: 5.0),
child: Text(poster.name,
style: Theme.of(context)
.textTheme
.headline4!
.copyWith(fontWeight: FontWeight.bold)),
),
Text(
'${poster.handle} ยท ${timeago.format(wave.createdAt)}',
style: Theme.of(context).textTheme.subtitle1,
),
Spacer(),
WaveTilePopup(poster: poster, wave: wave, user: user),
],
);
}
Widget waveText(Wave wave, BuildContext context) {
return Flexible(
child: TextSplitter(
wave.message,
context,
Theme.of(context).textTheme.subtitle2!,
));
}
Widget waveImage(Wave wave, BuildContext context) {
return Flexible(
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: InkWell(
child: CachedNetworkImage(
imageUrl: wave.imageUrl!,
fit: BoxFit.fill,
),
onTap: () {
Navigator.pushNamed(
context,
MyPhotoView.routeName,
arguments: {
'imageUrls': [wave.imageUrl!],
'index': 0
},
);
},
),
),
);
}
Widget waveButtons(Wave wave) {
return BlocBuilder<ProfileBloc, ProfileState>(
builder: (context, profileState) {
if (profileState is ProfileLoading) {
return Container();
}
if (profileState is ProfileLoaded) {
User user = profileState.user;
return Container(
margin: const EdgeInsets.only(top: 10.0, right: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Row(
children: [
//font awesome comment icon
Icon(
FontAwesomeIcons.comment,
size: 14.0,
color: Colors.grey,
),
SizedBox(width: 5),
Text(wave.comments.toString()),
],
),
BlocBuilder<WaveLikingBloc, WaveLikingState>(
builder: (context, waveLikingState) {
//set waveLikingState to be WaveLikingLoaded
waveLikingState = waveLikingState as WaveLikingLoaded;
bool inShortTermLikes =
waveLikingState.shortTermLikes.contains(wave.id);
bool inShortTermDislikes =
waveLikingState.shortTermDislikes.contains(wave.id);
bool inLikes = waveLikingState.likes.contains(wave.id);
bool inDislikes = waveLikingState.dislikes.contains(wave.id);
bool likedBy = wave.likedBy.contains(user.id);
bool isLiked = ((likedBy || inLikes || inShortTermLikes) &&
!(inDislikes || inShortTermDislikes));
int likeCount = (inLikes || inShortTermLikes)
? wave.likes + 1
: (inDislikes || inShortTermDislikes)
? wave.likes - 1
: wave.likes;
return LikeButton(
isLiked: isLiked,
size: 30,
circleColor: CircleColor(
start: Colors.red[300]!, end: Colors.red[900]!),
bubblesColor: BubblesColor(
dotPrimaryColor: Colors.red[300]!,
dotSecondaryColor: Colors.red[900]!,
),
likeBuilder: (bool isLiked) {
return Icon(
Icons.favorite,
color: isLiked ? Colors.red[900] : Colors.grey,
size: 20,
);
},
likeCount: likeCount,
countBuilder: (int? count, bool isLiked, String text) {
var color = isLiked ? Colors.red[900] : Colors.grey;
Widget result;
result = Text(
text,
style: TextStyle(color: color),
);
return result;
},
onTap: (bool isLiked) async {
(isLiked)
? BlocProvider.of<WaveLikingBloc>(context).add(
DislikeWave(waveId: wave.id, userId: user.id!))
: BlocProvider.of<WaveLikingBloc>(context)
.add(LikeWave(waveId: wave.id, userId: user.id!));
return !isLiked;
},
);
},
),
],
),
);
}
return Container();
},
);
}
When creating a wave with just text, it will end up looking like this:
This is the ideal situation right now. However, when an image is added, it looks like this:
Going through widget inspector does not seem to help me much, and the only way I can change the size of the wave is by deleting the image, leading me to believe the image is causing some weird interaction with the Flexible/Expanded widgets near it. Anyone got any ideas?
Thanks!
As you have 2 Flexible in a column , it will take up all the space. You can try removing Flexible from Text.

how to move buttons to a new row automatically when space is finished in flutter?

I am building an app where user is asked a question and an audio is player then the user selects the option he thinks is the correct one. i am having two problems here
i want to for the user when he taps the play button the icon changes to play and when ever the audio stops the play button come back to normal like in messanger apps.
the options button i will be having are too many that cannot fit in a single row so what i want is for the options to move to a new row after showing 5 or 6 button in each row. Any ideas for these two Questions.
this is my Question files:
// ignore_for_file: prefer_const_constructors, file_names, non_constant_identifier_names
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_tts/flutter_tts.dart';
import '../../utilities/button.dart';
import '../../utilities/buttonTapped.dart';
import '../../utilities/playbutton.dart';
import '../../utilities/playbutton.dart';
class Q7sound extends StatefulWidget {
const Q7sound({Key? key}) : super(key: key);
#override
State<Q7sound> createState() => _Q7soundState();
}
class _Q7soundState extends State<Q7sound> {
String question =
"Listen to this letter sound and then choose the letter whose sound you hear";
var changeButton = false;
var score = 0;
final FlutterTts flutterTts = FlutterTts();
speak(word) async {
await flutterTts.setLanguage("en-US");
await flutterTts.setPitch(1);
await flutterTts.setVolume(1.0);
await flutterTts.speak(word);
}
var word = "b";
var tap_count = 0;
bool isPlayingMsg = false;
bool showImage = false;
bool showoptions = false;
bool showquestion = true;
var letter = 'B';
bool buttonPressed1 = false;
void _letsPress1() {
setState(() {
buttonPressed1 = true;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: true,
backgroundColor: Colors.cyan,
title: Text("AD&DY"),
),
body: Padding(
padding: const EdgeInsets.all(14.0),
child: Column(children: [
Container(
alignment: Alignment.center,
child: Text(
"Q1: $question",
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(
height: 20,
),
ListTile(
onTap: () async {
speak(word);
print(word);
},
leading: const Icon(Icons.play_arrow),
title: const Text("Play",
textScaleFactor: 1.2,
style: TextStyle(
color: Colors.black,
)),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TxtOption("A"),
TxtOption("B"),
TxtOption("F"),
TxtOption("D"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
],
)
]),
),
);
}
Material TxtOption(word) {
return Material(
elevation: 12,
shadowColor: Colors.grey,
color: Colors.cyan,
borderRadius: BorderRadius.circular(changeButton ? 50 : 8),
child: InkWell(
onTap: () {
Colors.red;
},
child: Container(
height: 50,
width: 50,
alignment: Alignment.center,
child: Text(word)),
),
);
}
}
for first issue you can do this:
ListTile(
onTap: () async {
speak(word);
print(word);
},
leading: Icon(isPlayingMsg ? Icons.stop :Icons.play_arrow),
title: const Text(isPlayingMsg ?"Stop" :"Play",
textScaleFactor: 1.2,
style: TextStyle(
color: Colors.black,
)),
),
for second problem you can use wrap instead of row:
Wrap(
spacing: 16,
runSpacing: 16,
children: [
TxtOption("A"),
TxtOption("B"),
TxtOption("F"),
TxtOption("D"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
],
)
and for space between children use spacing and between rows use runSpacing.
Use Wrap instead of row.
Wrap(
spacing: 10,
runSpacing: 10,
children: [
TxtOption("A"),
TxtOption("B"),
TxtOption("F"),
TxtOption("D"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
TxtOption("E"),
],
)

Flutter - Image.memory not refreshing after source change

I have a page that allows users to upload documents (as images). I have structured my page in a way that for each document type that can be uploaded a Document_Upload widget is used to reduce the amount of repeated code.
On initial load I use a FutureBuilder to get all the documents the user has already uploaded from our REST Api and then populate each Document_Upload widget with the relevant data.
On successful upload our REST Api returns the new image back to the Flutter app as a Byte Array so it can be displayed.
The problem I am currently facing is that no matter what I try the image widget (Image.memory) does not display the new image, it just stays on the old one.
I have tried almost everything I can think of/ find online to resolve this issue, including:
Calling setState({}); after updating the imageString variable - I can see the widget flash but it remains on the original image.
Using a function to callback to the parent widget to rebuild the entire child widget tree - same result as setState, all the widgets flash, but no update.
Calling imageCache.clear() & imageCache.clearLiveImages() before updating the imageString.
Using CircleAvatar instead of Image.memory.
Rebuilding the Image widget by calling new Image.memory() inside the setState call.
I am starting to question if this is an issue related to Image.memory itself, however, using Image.File / Image.network is not an option with our current requirement.
Refreshing the page manually causes the new image to show up.
My code is as follows:
documents_page.dart
class DocumentsPage extends StatefulWidget {
#override
_DocumentsPageState createState() => _DocumentsPageState();
}
class _DocumentsPageState extends State<DocumentsPage>
with SingleTickerProviderStateMixin {
Future<Personal> _getUserDocuments;
Personal _documents;
#override
void didChangeDependencies() {
super.didChangeDependencies();
_getUserDocuments = sl<AccountProvider>().getUserDocuments();
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: SafeArea(
child: Center(
child: Padding(
padding: EdgeInsets.all(20),
child: Container(
constraints: BoxConstraints(maxWidth: 1300),
child: buildFutureBuilder(context)),
)),
),
);
}
Widget buildFutureBuilder(BuildContext context) {
var screenSize = MediaQuery.of(context).size;
return FutureBuilder<Personal>(
future: _getUserDocuments,
builder: (context, AsyncSnapshot<Personal> snapshot) {
if (!snapshot.hasData) {
return Text("Loading");
} else {
if (snapshot.data == null) {
return Center(child: Text('Error: ${snapshot.error}'));
} else {
_documents = snapshot.data;
return Column(
children: [
SizedBox(height: 20.0),
Text(
"DOCUMENTS",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.bold,
color: AppColors.navy),
),
Container(
constraints: BoxConstraints(maxWidth: 250),
child: Divider(
color: AppColors.darkBlue,
height: 20,
),
),
Container(
margin: EdgeInsets.only(top: 5.0, bottom: 5.0),
child: Text(
"These documents are required in order to verify you as a user",
style: TextStyle(fontSize: 14))),
Container(
margin: EdgeInsets.only(bottom: 25.0),
child: Text("View our Privacy Policy",
style: TextStyle(fontSize: 14))),
Container(
child: screenSize.width < 768
? Column(
children: [
DocumentUpload(
imageType: "ID",
imageString: _documents.id),
DocumentUpload(
imageType: "Drivers License Front",
imageString: _documents.driversLicenseFront,
),
DocumentUpload(
imageType: "Drivers License Back",
imageString: _documents.driversLicenseBack,
)
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
DocumentUpload(
imageType: "ID",
imageString: _documents.id),
DocumentUpload(
imageType: "Drivers License Front",
imageString: _documents.driversLicenseFront,
),
DocumentUpload(
imageType: "Drivers License Back",
imageString: _documents.driversLicenseBack,
),
])),
Container(
child: screenSize.width < 768
? Container()
: Padding(
padding:
EdgeInsets.only(top: 10.0, bottom: 10.0))),
Container(
child: screenSize.width < 768
? Column(
children: [
DocumentUpload(
imageType: "Selfie",
imageString: _documents.selfie,
),
DocumentUpload(
imageType: "Proof of Residence",
imageString: _documents.proofOfResidence,
),
Container(width: 325)
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
DocumentUpload(
imageType: "Selfie",
imageString: _documents.selfie,
),
DocumentUpload(
imageType: "Proof of Residence",
imageString: _documents.proofOfResidence,
),
Container(width: 325)
])),
],
);
}
}
});
}
}
document_upload.dart
class DocumentUpload extends StatefulWidget {
final String imageType;
final String imageString;
const DocumentUpload({this.imageType, this.imageString});
#override
_DocumentUploadState createState() => _DocumentUploadState();
}
class _DocumentUploadState extends State<DocumentUpload> {
String _imageType;
String _imageString;
bool uploadPressed = false;
Image _imageWidget;
#override
Widget build(BuildContext context) {
setState(() {
_imageType = widget.imageType;
_imageString = widget.imageString;
_imageWidget =
new Image.memory(base64Decode(_imageString), fit: BoxFit.fill);
});
return Container(
constraints: BoxConstraints(maxWidth: 325),
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
new BoxShadow(
color: AppColors.lightGrey,
blurRadius: 5.0,
offset: Offset(0.0, 3.0),
),
],
),
child: Card(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.0),
),
child: Column(children: <Widget>[
Padding(padding: EdgeInsets.only(top: 5.0)),
Row(
//ROW 1
children: <Widget>[
Expanded(
child: Text(
_imageType,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.darkBlue),
),
),
],
),
Row(
//ROW 2
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.only(left: 5.0, bottom: 5.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: _imageWidget,
)),
),
Consumer<AccountProvider>(
builder: (context, provider, child) {
return Padding(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Padding(
padding:
EdgeInsets.only(top: 5.0, bottom: 5.0),
child: Icon(Icons.star,
size: 20, color: AppColors.darkBlue)),
Padding(
padding:
EdgeInsets.only(top: 5.0, bottom: 5.0),
child: Text('Drag file here or',
textAlign: TextAlign.center)),
Padding(
padding:
EdgeInsets.only(top: 5.0, bottom: 5.0),
child: DynamicGreyButton(
title: uploadPressed
? "Uploading ..."
: "Browse",
onPressed: () async {
FilePickerResult result =
await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: [
'jpg',
'jpeg',
'png'
]);
if (result != null) {
uploadPressed = true;
Uint8List file =
result.files.single.bytes;
String fileType =
result.files.single.extension;
await provider
.doUploadDocument(
_imageType, file, fileType)
.then((uploadResult) {
if (uploadResult == null ||
uploadResult == '') {
showToast(
"Document failed to upload");
return;
} else {
showToast("Document uploaded",
Colors.green, "#66BB6A");
uploadPressed = false;
_imageString = uploadResult;
setState(() {});
}
});
} else {
// User canceled the picker
uploadPressed = false;
}
},
))
]));
})
],
),
])));
}
}
Image Upload HTTP Call
#override
Future uploadDocuments(DocumentsUpload model) async {
final response = await client.post(
Uri.https(appConfig.baseUrl, "/api/Account/PostDocuments_Flutter"),
body: jsonEncode(model.toJson()),
headers: <String, String>{
'Content-Type': 'application/json'
});
if (response.statusCode == 200) {
var data = json.decode(response.body);
return data;
} else {
return "";
}
}
EDIT: Attached GIF of current behaviour.
I am pretty much out of ideas at this point, any help would be greatly appreciated.
Came up with a solution.
I created a second variable to hold the new image string and showed an entirely new image widget once the second variable had value.
String _newImage;
In the success of the upload...
_newImage = uploadResult;
setState(() {});
Image widget...
child: (_newImage == null || _newImage == '')
? new Image.memory(base64Decode(_imageString), fit: BoxFit.fill)
: new Image.memory(base64Decode(_newImage), fit: BoxFit.fill)
Not a very elegant solution, but it's a solution, but also not necessarily the answer as to why the original issue was there.

How to pass data back from a widget?

I have a screen where users can add a location. Here, I have separated all my widgets into there own files as illustrated below;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:fluttershare/pages/location/location_help_screen.dart';
import 'package:fluttershare/widgets/common_widgets/customDivider.dart';
import 'package:uuid/uuid.dart';
import '../../widgets/camp_type_select.dart';
import '../../widgets/extra_location_notes.dart';
import '../../widgets/location_input.dart';
import '../../widgets/opening_times.dart';
import '../../widgets/post_media.dart';
import '../../widgets/space_avalibility.dart';
import '../../widgets/utility_type_select.dart';
import '../../widgets/width_restriction.dart';
import '../../widgets/height_restriction.dart';
import '../../models/locations.dart';
import '../../models/user.dart';
import '../home.dart';
class AddNewLocation extends StatefulWidget {
static const routeName = '/add-new-location';
final User currentUser;
AddNewLocation({this.currentUser});
_AddNewLocationState createState() => _AddNewLocationState();
}
class _AddNewLocationState extends State<AddNewLocation> {
String postId = Uuid().v4();
final _scaffoldKey = GlobalKey<ScaffoldState>();
PlaceLocation _pickedLocation;
int storyPostCount = 0;
bool isLoading = false;
void _selectPlace(double lat, double lng) {
_pickedLocation = PlaceLocation(lattitude: lat, longitude: lng);
}
getLocationPostCount() async {
setState(() {
isLoading = true;
});
QuerySnapshot snapshot = await locationPostRef
.document(currentUser.id)
.collection('user_location_posts')
.getDocuments();
setState(() {
storyPostCount = snapshot.documents.length;
});
}
createLocationPostInFirestore(
{String mediaUrl,
String description,
double heightRestriction,
double widthRestriction}) {
locationPostRef
.document(currentUser.id)
.collection("user_location_posts")
.document(postId)
.setData({
"postId": postId,
"ownerId": currentUser.id,
"username": currentUser.username,
"description": description,
"timestamp": timestamp,
"lattitude": _pickedLocation.lattitude,
"longitude": _pickedLocation.longitude,
"max_height": heightRestrictionValue.toStringAsFixed(0),
"max_width": widthRestrictionValue.toStringAsFixed(0),
});
}
handlePostSubmit() {
createLocationPostInFirestore(
heightRestriction: heightRestrictionValue,
widthRestriction: widthRestrictionValue,
);
SnackBar snackbar = SnackBar(
content: Text("Profile Updated"),
);
_scaffoldKey.currentState.showSnackBar(snackbar);
setState(() {
postId = Uuid().v4();
});
}
buildUploadUserHeader() {
return Container(
margin: EdgeInsets.only(bottom: 10),
height: 200,
child: Row(
children: <Widget>[
Expanded(
flex: 2,
child: Container(
color: Colors.blue,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
ListTile(
leading: CircleAvatar(
backgroundImage:
CachedNetworkImageProvider(currentUser.photoUrl)),
),
],
),
),
),
Expanded(
flex: 6,
child: Container(
color: Colors.pink,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text(currentUser.displayName),
],
),
),
),
],
),
);
}
buildCampUploadForm() {
return Container(
child: SingleChildScrollView(
child: Column(
children: <Widget>[
//buildUploadUserHeader(), //TODO: This is the profile header that is dissabled for now. Work on possibly a header in the future.
Container(
padding: EdgeInsets.all(15),
child: Column(
children: <Widget>[
CampTypeSelect(),
CustomDivider(),
LocationInput(_selectPlace),
CustomDivider(),
HeightRestriction(),
WidthRestriction(),
SpaceAvalibility(),
OpeningTimes(),
CustomDivider(),
PostMedia(),
CustomDivider(),
UtilityServices(),
CustomDivider(),
ExtraLocationNotes(),
Container(
height: 80,
margin: EdgeInsets.only(top: 10, bottom: 10),
child: Row(
children: <Widget>[
Expanded(
child: FlatButton(
color: Colors.black,
onPressed: () => handlePostSubmit(),
child: Text(
"SUBMIT",
style: Theme.of(context).textTheme.display2,
),
padding: EdgeInsets.all(20),
),
)
],
),
),
],
),
),
],
),
));
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text(
'Add New Location',
style: TextStyle(color: Colors.black),
),
actions: <Widget>[
// action button
IconButton(
icon: Icon(Icons.info_outline),
color: Colors.black,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) => LocationSubmitHelpScreen()),
);
},
),
// action button
IconButton(
icon: Icon(Icons.close),
color: Colors.black,
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
body: buildCampUploadForm(),
backgroundColor: Colors.white,
);
}
}
What I am trying to do is pass the data back from the widget ExtraLocationNotes()
to the function createLocationPostInFirestore().
For context, this is what my widget looks like;
import 'package:flutter/material.dart';
import 'common_widgets/custom_form_card.dart';
class ExtraLocationNotes extends StatefulWidget {
_ExtraLocationNotesState createState() => _ExtraLocationNotesState();
}
class _ExtraLocationNotesState extends State<ExtraLocationNotes> {
TextEditingController descriptionController = TextEditingController();
#override
Widget build(BuildContext context) {
return CustomFormCard(
child: Column(
children: <Widget>[
Container(
child: Row(
children: <Widget>[
Text(
"EXTRA INFORMATION",
style: TextStyle(
fontSize: 18.0,
color: Colors.black,
fontWeight: FontWeight.w400,
letterSpacing: 2.0,
),
),
],
),
),
SizedBox(height: 20),
TextFormField(
controller: descriptionController,
maxLines: 6,
maxLength: 250,
maxLengthEnforced: true,
style:
new TextStyle(fontSize: 18.0, height: 1.3, color: Colors.black),
decoration: const InputDecoration(
hintText:
"Please write a description of this location for fellow travellers.",
alignLabelWithHint: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.only(),
borderSide: BorderSide(color: Colors.black),
),
),
),
],
),
);
}
}
How do I pass the data back to the parent widget?
You need a callback, which will be triggered in the child widget then the value will be updated in the parent widget:
// 1- Define a pointers to executable code in memory, which is the callback.
typedef void MyCallback(String val);
class ExtraLocationNotes extends StatefulWidget {
// 2- You will pass it to this widget with the constructor.
final MyCallback cb;
// 3- ..pass it to this widget with the constructor
ExtraLocationNotes({this.cb});
_ExtraLocationNotesState createState() => _ExtraLocationNotesState();
}
class _ExtraLocationNotesState extends State<ExtraLocationNotes> {
//..
//...
RaisedButton(
//..
// 4- in any event inside the child you can call the callback with
// the data you want to send back to the parent widget:
onPressed: () {
widget.cb("Hello from the other side!");
}
),
}
Then inside the parent widget you need to catch the data which sent form the child:
class AddNewLocation extends StatefulWidget {
//...
_AddNewLocationState createState() => _AddNewLocationState();
}
class _AddNewLocationState extends State<AddNewLocation> {
// 1- Global var to store the data that we're waiting for.
String _dataFromMyChild = "";
buildCampUploadForm() {
return Container(
//...
//...
// 2- Pass the callback with the constructor of the child, this
// will update _dataFromMyChild's value:
ExtraLocationNotes(cb: (v) => setState(() => _dataFromMyChild = v)),
//..
}
// then
createLocationPostInFirestore() {
// Use _dataFromMyChild's value here
}
}
You can use the BuildContext object to get the context widget (might no be the parent!) couldn't read it all but as i understand that you need to pass the info from the child to the parent ,and you can do it with some like this :-
(context.widget as MyType).doStuff();
Note.
please check first with
print(context.widget.runtimeType);
but to make a better solution make a mutable data object that is passed from parent to the child so when changes happens it reflect's on the parent so you can separate business logic from ui logic.

Why get data from shared preferences have a delayed?

I have one screen to display data from shared preferences. I already success save and get data from shared preferences. Then I have a flow in one screen like this:
If the user clicks that screen, it will check the data from shared preferences.
If data is not null / not empty, it will display the data login like user profile, etc.
If data is null/empty, it will show a button login.
I get the logic for that flow, but the problem is, before data showing in the screen (number 2), it shows button login first for a few milliseconds then show the data. Why did it happen? It's not got data from API / internet and I'm not using FutureBuilder, I just using Shared Preferences. How to kill this delayed? Below is my full code:
class MorePage extends StatefulWidget {
#override
_MorePageState createState() => _MorePageState();
}
class _MorePageState extends State<MorePage> {
bool isLoading = false;
SessionManager _sessionManager = SessionManager();
int status;
#override
void initState() {
super.initState();
_sessionManager.getStatusLogin().then((value) { //i use this for get status code login success
setState(() {
status = value;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: color_grey_bg,
body: SafeArea(
child: getMorePage(),
),
);
}
Widget getMorePage() {
return ListView(
physics: ClampingScrollPhysics(),
children: <Widget>[
Container(
padding: EdgeInsets.only(
left: MediaQuery.of(context).size.width / 20,
),
height: MediaQuery.of(context).size.width / 4,
width: MediaQuery.of(context).size.width,
color: color_white,
child: setProfile(),
),
],
);
}
Widget setProfile() {
if (status == 200) { // i use this logic to show widget with status login, but it's have delayed like show data from API. How to kill it? Because I using SharedPreferences, not hit the API
return profileUser();
} else {
return notSignIn();
}
}
Widget profileUser() {
return Row(
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
name,
style: TextStyle(
color: color_grey_text,
fontSize: MediaQuery.of(context).size.width / 26,
fontWeight: FontWeight.bold,
),
),
Text(
email,
style: TextStyle(
color: color_grey_text,
fontSize: MediaQuery.of(context).size.width / 30,
fontWeight: FontWeight.normal,
),
),
Text(
role,
style: TextStyle(
color: color_grey_text,
fontSize: MediaQuery.of(context).size.width / 35,
fontWeight: FontWeight.normal,
),
),
],
),
Spacer(),
IconButton(
icon: Icon(
Icons.arrow_forward_ios,
size: MediaQuery.of(context).size.height / 40,
),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => DetailUserPage()));
},
),
],
);
}
Widget notSignIn() {
return Padding(
padding:
EdgeInsets.only(left: 50.0, right: 50.0, top: 30.0, bottom: 30.0),
child: RaisedGradientButton(
child: Text(
'Login',
style: TextStyle(
color: color_white,
fontSize: MediaQuery.of(context).size.width / 25),
),
gradient: LinearGradient(
colors: <Color>[color_blue, color_green],
),
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => LoginPage()));
},
),
);
}
}
And this is class SessionManager for create function of shared_preferences:
class SessionManager {
.....
getStatusLogin() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
int status = preferences.getInt("status");
return status;
}
....
}
the getprofile function is actually is a future , you used the async await keywords .
it's true that retrieving the data from the sharedpref doesn't take time ,but getting the instance of the sharedpref is the root of the cause . so you have to options to solve this solution .
1-getting the instance of the shared pref in the main function. you can get the instance of the shared pref and pass it as argument to the whole application.
Example :
void main ()async{
final instance = await sharedPreference.getInstance();
runApp(MyApp(instance));}
now in your MorePage widget
class _MorePageState extends State<MorePage> {
LoginStatus _loginStatus = LoginStatus.notSignIn;
SessionManager _sessionManager = SessionManager();
String name, email, role;
//no need for the async keyword
getProfile() { //this func just for show the data
name = widget.preferences.getString("fullname");
email = widget.preferences.getString("email");
role = widget.preferences.getString("role");
}
#override
void initState() {
super.initState();
getProfile();
_sessionManager.getLoginStatus().then((value) { //this code for get the status of login
setState(() {
_loginStatus = value;
});
});
}
now the getProfile function is not async which means there's no milliseconds that makes that weird behavior at the beginning .
2-Make another enum value 'busy' (simpler solution) . simply you can leave you code as it's but add a new enum value which is busy to give hint to the user that the app is checking if he has logined before or not ,you simply will give him that hint in the set profile function , you'll create another condition if ( _loginStatus == LoginStatus.busy ) return Text('checking user Info').
hope that helps !
Edit :
You this package get_it to make a singleton instance of the session manager class and you can access it anywhere .
GetIt locator = GetIt();
void setUpLocator() {
locator.registerLazySingleton(() => SessionManager());
}
void main() async {
setUpLocator();
await locator.get<SessionManager>().getStatusLogin();
runApp(MyApp());
}
class MorePage extends StatefulWidget {
#override
_MorePageState createState() => _MorePageState();
}
class _MorePageState extends State<MorePage> {
bool isLoading = false;
final _sessionManager = locator.get<SessionManager>();
int status;
#override
void initState() {
super.initState();
//make property of statuesif you don't have property of the statues
//in the session manager class
status = _sessionManager.statues;
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: color_grey_bg,
body: SafeArea(
child: getMorePage(),
),
);
}
Widget getMorePage() {
return ListView(
physics: ClampingScrollPhysics(),
children: <Widget>[
Container(
padding: EdgeInsets.only(
left: MediaQuery.of(context).size.width / 20,
),
height: MediaQuery.of(context).size.width / 4,
width: MediaQuery.of(context).size.width,
color: color_white,
child: setProfile(),
),
],
);
}
Widget setProfile() {
if (status == 200) {
// i use this logic to show widget with status login, but it's have delayed like show data from API. How to kill it? Because I using SharedPreferences, not hit the API
return profileUser();
} else {
return notSignIn();
}
}
Widget profileUser() {
return Row(
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
name,
style: TextStyle(
color: color_grey_text,
fontSize: MediaQuery.of(context).size.width / 26,
fontWeight: FontWeight.bold,
),
),
Text(
email,
style: TextStyle(
color: color_grey_text,
fontSize: MediaQuery.of(context).size.width / 30,
fontWeight: FontWeight.normal,
),
),
Text(
role,
style: TextStyle(
color: color_grey_text,
fontSize: MediaQuery.of(context).size.width / 35,
fontWeight: FontWeight.normal,
),
),
],
),
Spacer(),
IconButton(
icon: Icon(
Icons.arrow_forward_ios,
size: MediaQuery.of(context).size.height / 40,
),
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => DetailUserPage()));
},
),
],
);
}
Widget notSignIn() {
return Padding(
padding:
EdgeInsets.only(left: 50.0, right: 50.0, top: 30.0, bottom: 30.0),
child: RaisedGradientButton(
child: Text(
'Login',
style: TextStyle(
color: color_white,
fontSize: MediaQuery.of(context).size.width / 25),
),
gradient: LinearGradient(
colors: <Color>[color_blue, color_green],
),
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => LoginPage()));
},
),
);
}
}
I think show Circle Progress once getting values form shared pref then show main container.
Please check below code:-
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: color_grey_bg,
body: SafeArea(
child: LoginStatus.notSignIn ? const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Color(colorPrimary))),
) : getMorePage()
),
);
}
I think you should create a class and then use reference -:
Future<void> main() async{
ShareP.preferences = await SharedPreferences.getInstance();
}
class ShareP {
static SharedPreferences preferences;
}
Now you can refer it ("ShareP.preferences") and get your SharedPreference values