I'm working on a Flutter project where I want to pre-cache images at the start of the app.
The idea is when you start the app the first time, it downloads a list of images either cache / stored in DB / stored in local storage / or any other viable solution. I don't really know the best practice here. And then when you start the app the next time you already have the photos so you don't want to download them again (based on the version of the backend data).
From what I saw;
The cache, I don't really know if it is not persistent enough and I don't know if I'll have enough control over it.
The local storage, I think I'll have to ask the user permission to access the files of the device
The database, I'll have to encode/decode the photos every time I want to save/get them so it'll take some computation.
My ideal choice would be the database as I'd have control over the data and it's a rather small app so the computation is minimal and I won't have to ask the user permission.
I've tried over the last days to implement this using every of the solution stated above and I can't make it work.
Right now I want to store an Image into the database (I'm using sqflite) without displaying it and then read it to display it as a Widget from another Screen. I have 2 screens, the first one that I called SplashScreen to fetch and save the images and the second one which is HomeScreen to read the images from the database and display them.
SplashScreen.dart:
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> with SingleTickerProviderStateMixin {
bool hasInternet = true;
var subscription;
double loading = 0;
#override
initState() {
super.initState();
getPhotos();
subscription = Connectivity().onConnectivityChanged.listen((ConnectivityResult connectivityResult) {
setState(() {
hasInternet = connectivityResult == ConnectivityResult.mobile || connectivityResult == ConnectivityResult.wifi;
});
});
}
dispose() {
super.dispose();
subscription.cancel();
}
Future getPhotos() async {
List<String> photoUrls = await fetchPhotos();
photoUrls.asMap().forEach((index, photoUrl) async {
var response = await http
.get(photoUrl);
loading = index / photoUrls.length;
// Convert Photo response and save them in DB
imageDBFormat = ...
savePhotosInDB(imageDBFormat)
});
}
#override
Widget build(BuildContext context) {
if(loading == 1) {
Navigator.pushNamed(context, '/');
}
return Center(
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/back.png"),
fit: BoxFit.cover,
)
),
child: Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: 300,
child: LinearProgressIndicator(
value: loading,
valueColor: AlwaysStoppedAnimation<Color>(ProjectColors.primaryDark),
),
)
],
),
],
)
),
),
);
}
}
HomeScreen.dart:
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateMixin {
List<Widget> images;
initState() {
super.initState();
getImages();
}
void getImages() async {
List imgs = getImagesFromDB();
setState(() {
images = imgs.map((image) {
// Convert imgs from db into Widget
Widget imageWidget = ...
return Container(
child: imageWidget,
);
}).toList();
});
}
#override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/back.png"),
fit: BoxFit.cover,
)
),
child: Scaffold(
backgroundColor: Theme.of(context).backgroundColor,
body: Column(
children: images == null ? images : <Widget>[],
),
)
);
}
}
I'm OK to reconsider any point to follow the best practices.
Thanks a lot for your help.
You can use https://pub.dev/packages/cached_network_image library,it's using sqlite as a storage already and you can configure duration of persistance.
class CustomCacheManager extends BaseCacheManager {
static const key = "customCache";
static CustomCacheManager _instance;
factory CustomCacheManager() {
if (_instance == null) {
_instance = new CustomCacheManager._();
}
return _instance;
}
CustomCacheManager._() : super(key,
maxAgeCacheObject: Duration(months: 6),
maxNrOfCacheObjects: 100);
Future<String> getFilePath() async {
var directory = await getTemporaryDirectory();
return p.join(directory.path, key);
}
}
and after that you can use in your build method:
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
cacheManager: CustomCacheManager()
),
Related
so I'm currently working on an application that has a listview on the first screen (implemented on main.dart).
The listview fetches it's data from internet (async).
The problem is that, the listview does not get updated when the data is changed.
(I can implement this functionality simply by designing a 'reload' button and pressing it every time I want the new data. But that's not what I want right now).
In other words, how can I update the listview automatically?
EDIT1: ADDING SOME CODE
code might be messy; see the description at the end.
class RssFeed extends StatelessWidget {
String title;
String pubDate;
RssFeed(this.title, this.pubDate);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Align(
alignment: Alignment.topRight,
child: Text(title),
),
Text(pubDate)
],
),
);
}
}
class FeedsList extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _FeedsListState();
}
}
class _FeedsListState extends State<FeedsList> {
List<Widget> list1 = new List<Widget>();
#override
void initState() {
super.initState();
ls();
}
Future ls() async {
list1.clear();
list.clear();
sites1.clear();
RSS_reader rss_reader = new RSS_reader();
for (var i in saver.list.items) {
sites1.add(
site(siteAdress: i.siteAdress, siteDescription: i.siteDescription));
}
var res = await rss_reader.Get_items(sites1);
for (var val in res) {
list.add(InkWell(
onTap: () => _launchURL(val.item.link),
child: Container(
height: 50,
color: Colors.amber[100],
child: Center(
child: new RssFeed(val.item.title, val.item.pubDate.toString()),
),
)));
}
print(list.length);
setState(() {
list1 = list;
});
}
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: list1.length,
itemBuilder: (BuildContext context, int i) {
return list1[i];
}));
}
}
DESCRIPTION:
As you can guess, this is a RSS reader.
So, I have a class RSSFeed; which makes one of the tiles of Listview.
then in the FeedsList class (stateful widget), I make the listview.
I have a class called RSS_reader and a method Get_items, which gets a bunch of sites as input and puts those sites' newest feeds in a list ('res' in the above code).
Then, I put the items in a list of 'Container's and then build the listview.
Then, in the main function, I create a container like below:
Container(
height: 500,
width: 580,
child: FeedsList(),
)
and there appears the problem; the FeedsList class does not get updated automatically. although if I put a button and navigate to FeedsList class through that button, the list is refreshed and OK.
Thanks for reading and help.
If you just want to fetch data once from your external source use a FutureBuilder, if you want to fetch data multiple times take a look to StreamBuilder. Both widgets will have the behavior you are looking for, with no refresh button.
Simple example of how to use a FutureBuilder:
Future<List<String>> _fetchData() {
return // fetch data from source
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _fetchData,
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.hasData && snapshot.data != null) {
// This widget will be built when data is fetched
const List<String> list = snapshot.data;
return ListView(
children: list.map(
(element) => ListTile(
title: Text(element),
),
).asList(),
);
} else {
// This widget will be built while you are waiting for your data to be fetched
return Container(
child: Center(
child: Text("Loading data..."),
),
);
}
},
);
}
You have to stream data and ListView will update automatically.
In the button that you say you can re call your ls() functions, your list should update on tap button
sample:
return Scaffold(
body: ListView.builder(
itemCount: list1.length,
itemBuilder: (BuildContext context, int i) {
return list1[i];
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
onPressed: () => ls(),
),
);
I'm trying to load an image from firebase storage to my app however I have this weird issue where the profile page(where this image is loading) keeps flickering. The image is loading fine however the whole widget keeps flickering. I have narrowed the issue down to the setState() called within the function getProfilePic() after some debugging, however I do not know if it's the function itself or my call to said function.
P.S there is no issue with the fileURL or picRef.getDownloadURL() as I've tested this with a random internet image as well and got the same flickering.
class profileTab extends StatefulWidget {
#override
_profileTabState createState() => _profileTabState();
}
class _profileTabState extends State<profileTab> {
User user = FirebaseAuth.instance.currentUser;
String _image = "https://picsum.photos/250?image=9";
Reference picRef = FirebaseStorage.instance.ref().child(FirebaseAuth.instance.currentUser.uid);
Future<Widget> getProfilePic() async {
await picRef.getDownloadURL().then((fileURL){
setState(() {
_image = fileURL;
});
});
}
#override
Widget build(BuildContext context) {
getProfilePic();
return StreamBuilder(
stream: FirebaseFirestore.instance.collection('users').doc(user.uid).snapshots(),
builder: (context, snapshot){
if (snapshot.connectionState == ConnectionState.active){
return ListView(
children: <Widget>[
SizedBox(height: 100.0,),
CircleAvatar(
radius: 100.0,
backgroundColor: Colors.lightBlueAccent,
child: ClipOval(
child: SizedBox(
width: 180.0,
height: 180.0,
child: Image.network(_image,fit: BoxFit.fill,),
),
),
),
SizedBox(height: 30.0,),
Center(child: Text("Name: " + snapshot.data.data()['name'],textScaleFactor: 3.0,)),
]
);
}
else {
return CircularProgressIndicator();
}
},
);
}
}
getProfilePic is redrawing widget by calling setState.
setState calls build method which calls getProfilePic.
Therefore, when first time build method is called we call getProfilePic which again updates widget tree.
Fix: Inside getProfilePic add check to call setState if _image is null which will redraw widget only once.
It would be better if you use Image.network. You can refer this
https://www.woolha.com/tutorials/flutter-display-image-from-network-url-show-loading
In my Flutter application I am using Provider version 4.0.4 to manage the state of my app. In basic terms, my app will list down the nearby companies with their rating. users can select a organisation, open it and add their rating as well, so the final rating will be updated. I am using the Consumer concept in Provider to handle the tasks.
In NearByPlacesPage class I am listing down the companies around me with rating information. User can click on a company and they will be taken to OrganizationPage page.
In OrganizationPage class, the rating is displayed again. user can add their rating to the system. Then the rating information in both OrganizationPage page and NearByPlacesPage (back page) need to be updated.
The issue is, when the user update the rating, the rating in OrganizationPage get updated but not NearByPlacesPage in back stack. When we go back to NearByPlacesPage, we can clearly see the old rating values. The page need to be reloaded to get updated values.
Below are the important sections in my code
NearByPlacesPage
class NearByPlacesPage extends StatelessWidget {
int orgTypeID;
String orgTypeName;
NearByPlacesPage(this.orgTypeID, this.orgTypeName);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => RatingService()),
],
child: SingleChildScrollView(
child: _NearByPlacesPageUI(orgTypeID, orgTypeName),
),
),
appBar: AppBar(
title: Text(orgTypeName),
),
);
}
}
class _NearByPlacesPageUI extends StatefulWidget {
int orgTypeID;
String orgTypename;
_NearByPlacesPageUI(this.orgTypeID, this.orgTypename);
#override
State<StatefulWidget> createState() {
return _NearByPlacesPageState();
}
}
class _NearByPlacesPageState extends State<_NearByPlacesPageUI> {
#override
Widget build(BuildContext context) {
Consumer<RatingService>(builder: (context, data, child){
return Flexible(
child: ListView.builder(
itemCount: orgList.length,
itemBuilder:(BuildContext context, int index) {
Organization organization = orgList[index];
if (organization.isDisabled != true) {
RatingValue ratingValue = data.getData();
return Container(
margin: EdgeInsets.only(
top: 5, left: 5, right: 5),
child: _buildPlace(organization, ratingValue));
} else {
return Container();
}
},),
);
},);
}
}
OrganizationPage
class OrganizationPage extends StatelessWidget {
Organization organization;
String orgTypeName;
OrganizationPage(this.organization, this.orgTypeName);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: _OrganizationPageUI(organization, orgTypeName),
),
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(organization.name),
),
);
}
}
class _OrganizationPageUI extends StatefulWidget {
Organization organization;
String orgTypeName;
_OrganizationPageUI(this.organization, this.orgTypeName);
#override
State<StatefulWidget> createState() {
return _OrganizationPageState();
}
}
class _OrganizationPageState extends State<_OrganizationPageUI> {
#override
Widget build(BuildContext context) {
Consumer<RatingService>(
builder: (context, data, child) {
Consumer<RatingService>(
return Row(
children: <Widget>[
Container(
margin: EdgeInsets.only(top: 10, left: 10),
child: Text(daa.getData()
style: Theme.of(context).textTheme.bodyText2.apply(color: Colors.grey),
),
),
],
);
),
}
}
}
In OrganizationPage there is a AlerDialog, which allows the user to rate and save. When saved, it will call another method which will reload the data.
Widget _ratingDialog(double _rating) {
RatingService _ratingService =
Provider.of<RatingService>(context, listen: false);
Rating _rating = _ratingService.returnRating();
double _ratingValue = _ratingService.returnRating().rating;
return AlertDialog(
title: const Text("Your Rating"),
actions: [
new FlatButton(
child: const Text("Save"),
//onPressed: () => Navigator.pop(context),
onPressed: () async {
Rating rating = Rating(
idrating:
_rating.idrating != null ? _rating.idrating : null,
user: _user,
organization: widget.organization,
rating: _ratingValue,
dateCreated: DateTime.now().millisecondsSinceEpoch,
lastUpdated: DateTime.now().millisecondsSinceEpoch);
await _ratingService.saveOrUpdateRating(rating, authToken);
_loadRatingByUserAndOrganization(authToken);
_loadRatingValueByOrganization(authToken);
Navigator.pop(context);
},
),
],
);
}
Future _loadRatingByUserAndOrganization(String authToken) {
RatingService _ratingService =Provider.of<RatingService>(context, listen: false);
return _ratingService.getRatingByUserAndOrganization(
_authService.getDatabaseUser().user.iduser,
widget.organization.idorganization,
authToken);
}
RatingService
This is the class which is responsible for calling notifyListeners(). It will be triggered by the above AlertDialog and the expected behaviour is to reload data in both OrganizationPage and NearByPlacesPage
class RatingService with ChangeNotifier {
List<RatingValue> _ratingValueList ;
List<RatingValue> getData()
{
return _ratingValueList;
}
//Load rating by user and Organization
Future<void> getRatingByUserAndOrganization(int idUser, int organizationID, String authToken) async {
try {
var data = await http.get(
_navLinks.getRatingByUserAndOrganization(idUser, organizationID),
headers: {HttpHeaders.authorizationHeader: "Bearer $authToken"},
);
print(data.body);
_rating = Rating.fromJson(convert.json.decode(data.body));
notifyListeners();
} catch (error) {
print(error);
throw error;
}
}
}
What I have I done wrong?
I'm experimenting with flutter, creating a small screen to fetch cryptocurrency data. I have three crypto name (BTC, ETH, LTC), each will be displayed on card, the rate on USD.
I can get the data for each currency, and display it. But in my code, if each data fetch took 1 second, the cards display will be updated after 3 seconds. All data will be fetched first, then screen updated using setState().
This is the initial working code
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'coin_data.dart' as coinData;
class PriceScreen extends StatefulWidget {
#override
_PriceScreenState createState() => _PriceScreenState();
}
class _PriceScreenState extends State<PriceScreen> {
String _selectedCurrency = "USD";
List<coinData.CoinData> _coinData = [];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Coin Ticker'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildCoinCards(),
],
),
);
}
Card buildCoinCard(coinData.CoinData currentCoinData) {
return Card(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 28.0),
child: Text(
'1 ${currentCoinData.coinName()} = ${currentCoinData.last} $_selectedCurrency',
),
),
);
}
buildCoinCards() {
List<Card> coinCards = [];
for (var currentCoinData in _coinData) {
coinCards.add(
buildCoinCard(currentCoinData),
);
}
return Column(
children: coinCards,
);
}
#override
void initState() {
super.initState();
for (String crypto in coinData.cryptoList) {
_coinData.add(
coinData.CoinData(
last: 0,
displaySymbol: "$crypto-$_selectedCurrency",
),
);
}
updateCoinData();
}
void updateCoinData() async {
var coinDataFetch =
await coinData.CoinData().getAllCoinData(_selectedCurrency);
setState(() {
_coinData = coinDataFetch;
});
}
}
I'd like to update each item everytime a HTTP call is success. So the card will be updated one by one (took 1 second each) instead of waiting for 3 seconds. So I change the updateCoinData() to this, but it's not working.
void updateCoinData() async {
for (coinData.CoinData currentCoinData in _coinData) {
var fetchCoinData = await currentCoinData.getCoinData(
currentCoinData.coinName(), _selectedCurrency);
setState(() {
currentCoinData = fetchCoinData;
print(
currentCoinData.coinName() + " " + currentCoinData.last.toString());
});
}
}
I suppose I cannot update the List item on setState()?
How do I achieve this update one-by-one?
Thanks
I'm building an app for training in Flutter and I'm actually stuck in the filter functionality.
I have a ListView where I fetch data from TheMovieDB API and a ModalBottomSheet with three FilterChips for selecting the filter criteria (popular, top rated and latest movies).
And here's where I'm stuck. I want to call the "_loadNextPage()" method when the user presses the "Done" button in the ModalBottomSheet through "performUpdate()" but I can't do it because they're not in the same class.
I'll post the code down below for better understanding.
class _HomePageState extends State<HomePage> {
RequestProvider _requestProvider = new RequestProvider();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("FluttieDB"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.filter_list),
onPressed: () => buildFilterBottomSheet(),
)
],
),
body: MovieList(_requestProvider, _currentFilter),
);
}
void buildFilterBottomSheet() {
showModalBottomSheet(
context: context,
builder: (builder) {
return Container(
height: 150.0,
decoration: BoxDecoration(color: Colors.white),
child: Column(
children: <Widget>[
buildFilterTitle(context),
Expanded(
child: _FilterChipRow(),
),
],
),
);
});
}
Widget buildFilterTitle(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
alignment: Alignment.centerLeft,
height: 46.0,
decoration: BoxDecoration(color: Colors.blue),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Text(
"Filter by",
style: TextStyle(color: Colors.white, fontSize: 20.0),
),
OutlineButton(
onPressed: () => performUpdate(context),
padding: const EdgeInsets.all(0.0),
shape: const StadiumBorder(),
child: Text(
"Done",
style: TextStyle(color: Colors.white),
),
),
],
),
);
}
void performUpdate(BuildContext context) {
MovieList _movieList = new MovieList(_requestProvider, _currentFilter);
_movieList.createState()._loadNextPage();
Navigator.pop(context);
}
}
class MovieList extends StatefulWidget {
MovieList(this.provider, this.currentFilter, {Key key}) : super(key: key);
final RequestProvider provider;
final String currentFilter;
#override
_MovieListState createState() => new _MovieListState();
}
class _MovieListState extends State<MovieList> {
List<Movie> _movies = List();
int _pageNumber = 1;
LoadingState _loadingState = LoadingState.LOADING;
bool _isLoading = false;
_loadNextPage() async {
_isLoading = true;
try {
var nextMovies = await widget.provider
.provideMedia(widget.currentFilter, page: _pageNumber);
setState(() {
_loadingState = LoadingState.DONE;
_movies.addAll(nextMovies);
_isLoading = false;
_pageNumber++;
});
} catch (e) {
_isLoading = false;
if (_loadingState == LoadingState.LOADING) {
setState(() => _loadingState = LoadingState.ERROR);
}
}
}
#override
void initState() {
super.initState();
_loadNextPage();
}
#override
Widget build(BuildContext context) {
switch (_loadingState) {
case LoadingState.DONE:
return ListView.builder(
itemCount: _movies.length,
itemBuilder: (BuildContext context, int index) {
if (!_isLoading && index > (_movies.length * 0.7)) {
_loadNextPage();
}
return MovieListItem(_movies[index]);
});
case LoadingState.ERROR:
return Center(
child: Text("Error retrieving movies, check your connection"));
case LoadingState.LOADING:
return Center(child: CircularProgressIndicator());
default:
return Container();
}
}
}
As you can see, I did some experiments in the performUpdate() but it doesn't refresh the ListView with the selected option in the filters and I don't think it's the best way to achieve what I want.
Thanks and sorry if the question is a bit dumb. I'm a little bit newbie in Flutter.
Redux is a great state management library that originated with React and JS, but has been ported to Dart, and has a flutter specific library as well. Redux is a very powerful framework which uses a pub/sub system to allow your view to subscribe to changes to the model, while using a system of "actions" and "reducers" to update the model.
A great tutorial for getting up and running with Redux in Flutter can be found here
Alternatively you could look into the scoped model, which is another state management library for flutter. The scoped model is less capable, but for simple use cases may be more than adequate.
Further reading:
Understand and choose a state management solution
You Might Not Need Redux