I am developing a language learning quiz app. The person selects a topic (30 words) and I make a request to the server via the API to get a list of translations and transcriptions.
Because it takes a long time to get the data, I want to only get data for a couple of words. The first card is shown, and the second is already loaded. When the user has worked with the first one (swipe it), the second one appears, and the data for the third one is loaded in parallel.
How can you get data like that? All tinder-cards widgets request a stack, i.e. already prepared data. In my case, this is not allowed.
I'm assuming it should look like this: displaying not a stack but only a single card like
Ui with stack is fine, you need to do a little bit with fetch data logic, using Future, List and setState.
Example: when i swipe, the top data object was swiped and new data object was create at the last (begin fetch and seft setState when done).
import 'package:flutter/material.dart';
import 'dart:math';
void main() => runApp(MaterialApp(home: MyApp()));
class MyApp extends StatefulWidget {
final int cardSizes = 3;
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
var datas = <AGirl>[];
#override
void initState() {
// generate stack when init
datas = List.generate(widget.cardSizes, (_) => getNewGirl());
}
// function that return a AGirl and register a `setState` when data fetched
AGirl getNewGirl() => AGirl()..fetch().then((_) => setState(() {}));
// function that `swipe` top data object and add new unfetched data object at the last
void swipe() {
datas.removeAt(0);
datas.add(getNewGirl());
setState(() {});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: renderCards(),
),
);
}
List<Widget> renderCards() {
render(AGirl data, int index) {
return Positioned(
left: index * 200,
child: SizedBox(
width: 200,
height: 300,
child: Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text('Card $index'),
Text('AGirl id ${data.id}'),
data.hasData
? const Center(child: Text('loaded'))
: const CircularProgressIndicator(),
ElevatedButton(
onPressed: index == 0 ? swipe : null,
child: const Text('swipe'),
),
],
),
),
),
);
}
return datas.reversed
.map((data) => render(data, datas.indexOf(data)))
.toList();
}
}
class AGirl {
final int id;
dynamic data;
AGirl() : id = Random().nextInt(99999);
bool get hasData => data != null;
Future<AGirl> fetch() async {
await Future.delayed(const Duration(milliseconds: 1000));
data = {};
return this;
}
}
Related
Apologies in advance for posting Pseudo code. Real code would be too long.
I have a screen where I have a drop down at the top where a user can select an option. The rest of the page updates based on that option. Something like this:
// state variable
String idFromDropdown;
Column(
children: [
DropDownWidget(),
ChildWidget1(myId: idFromDropDown),
ChildWidget2(myId: idFromDropDown),
ChildWidget3(myId: idFromDropDown),
]
)
In the child widgets, I am using widget.myId to pass into a backend service and read new data.
Expectation is that when the dropdown changes and I call
setState((val)=>{idFromDropdown = val});
then the value would cascade into the three child widgets. Somehow trigger the widgets to reconnect to the backend service based on the new value of widget.myId.
How do I trigger a state update on the child widgets?
I ended up using a ValueNotifier. Instead of directly using a string and passing that into the child widgets. I ended up doing something like:
ValueNotifier<String> idFromDropdown;
...
setState((val)=>{idFromDropdown.value = val});
Then in each widget, I am adding a listener onto the ValueNotifier coming in and retriggering the read to the backend service.
While this works, I feel like I'm missing something obvious. My childwidgets now take in a ValueNotifier instead of a value. I'm afraid this is going to make my ChildWidgets more difficult to use in other situations.
Is this the correct way of doing this?
Use provider package your problem will solved easily
Here is example of Riverpod.
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final fetureDataForChild =
FutureProvider.family<List<String>, String>((ref, id) {
return Future.delayed(Duration(seconds: 1), () {
return <String>["active", "${id}", "got it "];
});
});
class MainWidgetR extends StatefulWidget {
#override
_MainWidgetState createState() => _MainWidgetState();
}
class _MainWidgetState extends State<MainWidgetR> {
String id = "id 0";
final items = List.generate(
4,
(index) => DropdownMenuItem(
child: Text("Company $index"),
value: "id $index",
),
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DropdownButton(
items: items,
value: id,
onChanged: (value) {
setState(() {
id = value as String;
});
},
),
RiverPodRespone(
id: id,
),
],
),
),
);
}
}
class RiverPodRespone extends ConsumerWidget {
final String id;
RiverPodRespone({
required this.id,
});
#override
Widget build(BuildContext context, watch) {
final futureData = watch(fetureDataForChild("$id"));
return futureData.map(
data: (value) {
final items = value.value;
return Column(
children: [
...items.map((e) => Text("$e")).toList(),
],
);
},
loading: (value) => CircularProgressIndicator(),
error: (value) => Text(value.toString()),
);
}
}
I have simple example about toggle widget,my expectation after click button i show circularProgressIndicator then after 3 second i showing Text. For my example i use riverpod_hooks and flutter_hooks.
LoadingProvider
class IsLoading extends StateNotifier<bool> {
IsLoading() : super(false);
void toggleLoading(bool value) => state = value;
}
final isLoadingProvider = StateNotifierProvider((ref) => IsLoading());
Main.dart
class TestingApp extends HookWidget {
#override
Widget build(BuildContext context) {
final IsLoading isLoading = useProvider(isLoadingProvider);
return Scaffold(
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: isLoading.state ? CircularProgressIndicator() : Text('This Is Your Data'),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: FlatButton(
onPressed: () => showLoading(isLoading), child: Text('Toggle Loading')),
),
],
),
),
);
}
void showLoading(IsLoading isLoading) async {
isLoading.toggleLoading(true);
print("Proses");
await Future.delayed(const Duration(seconds: 3));
print("Done");
isLoading.toggleLoading(false);
}
}
But when i press the button , it's not show progressIndicator and still showing the text and i got this warning :
The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart'.
I missed something ?
My source code following the documentation like this :
It says right there that it won’t trigger the method to rebuild when you call increment like that.
so instead of isLoading.toggleLoading(true)
call with a read method so it can rebuild the state like that
isLoading.read(context).toggleLoading(true)
Provide two types for your StateNotifierProvider, the type of class you are returning and the state the class has.
class IsLoading extends StateNotifier<bool> {
IsLoading() : super(false);
void toggleLoading(bool value) => state = value;
}
final isLoadingProvider =
StateNotifierProvider<IsLoading, bool>((ref) => IsLoading());
in your build method (of a ConsumerStateWidget) use:
final isLoading = ref.watch(isLoadingProvider);
return Scaffold(body: Center(child:
isLoading ? CircularProgressIndicator() : Text('This Is Your Data'),))
I have a flutter widget that attempts to solve soduku grids. I have class called SodukuSolver which does all the calculations and provides a List<String> of the current results. I call setState to refresh the list, but it does not update the screen.
Below, I'll try to include as much of the relevant code as I can. Full source is at https://github.com/mankowitz/soduku
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(title: "Soduku solver", home: Soduku());
}
}
class SodukuState extends State<Soduku> {
SodukuSolver ss;
List<String> _list;
int _changes = 0;
int _remaining = 81;
#override
Widget build(BuildContext context) {
final String _starting =
"750943002024005090300020000140089005093050170500360024000070009070400810400198057";
ss = new SodukuSolver(_starting);
_list = ss.getList();
return Scaffold(
appBar: AppBar(title: Text('Soduku solver'), actions: <Widget>[
// action button
IconButton(
icon: Icon(Icons.directions_walk),
onPressed: () {
_iterate();
},
),
]),
body: _buildGrid(),
);
}
Widget _buildGrid() {
return Column(children: <Widget>[
AspectRatio(
aspectRatio: 1.0,
child: Container(
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 9,
),
itemBuilder: _buildGridItems,
itemCount: 81,
),
),
),
]);
}
Widget _buildGridItems(BuildContext context, int index) {
return GestureDetector(
child: GridTile(
child: Container(
child: Center(
child: Text(_list[index]),
),
),
),
);
}
void _iterate() {
setState(() {
_changes = ss.iterateSoduku();
_remaining = ss.empties();
_list = ss.getList();
});
}
}
class Soduku extends StatefulWidget {
#override
SodukuState createState() => SodukuState();
}
So the problem is that _iterate() is being called, and I can use the debugger to see that the internal state of SodukuSolver is being updated and it is even passing _list correctly, but the grid on screen doesn't update, even though _changes and _remaining do update.
You are creating new SodukuSolver with same _starting every time the widget builds and then obtaining _list from it. So you are overriding changes from previous iteration.
Looks like SodukuSolver creation should be performed once. You can override initState in SodukuState and initialise SodukuSolver there or initialise it in the same place where it is declared
Just add your code in the initState() method as following
#override
void initState() {
super.initState();
final String _starting =
"750943002024005090300020000140089005093050170500360024000070009070400810400198057";
ss = new SodukuSolver(_starting);
_list = ss.getList();
}
In your case, your list is not getting updated as setState() method will call your SodukuSolver() and ss.getList(); methods every time. because, setSate() ultimately calls build method to render every time.
So adding it inside your initState method will solve your issue. As it is getting called only once when the screen/route initialises.
On the home page I have 4 different api calls fetching 4 different data from a wordpress site. The current way I have coded is that everytime we enter home page, in the initState(), the get _getHomePageData() function is called where the async api fetching is happening. while this is happening allDataLoaded boolean is set to false in the first place.
Once the data is loaded allDataLoaded is set to true, the loading stops and the widgets are shown.
Here is the homepage widget:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final String homePageLatestArtworkApi =
'https://example.com/wp-json/wp/v2/artworks?
per_page=1&_embed';
final String homePageAllArtworkApi =
'https://example.com/wp-json/wp/v2/artworks?
per_page=6&_embed';
final String homePageAllEventsApi =
'https://example.com/wp-json/wp/v2/events?
per_page=6&_embed';
final String homePageAllVenuesApi =
'https:example.com/wp-json/wp/v2/venues?
per_page=6&_embed';
List homePageLatestArtwork;
List homePageAllArtworks;
List homePageAllEvents;
List homePageAllVenues;
var allDataLoaded = false;
#override
void initState(){
super.initState();
print('init state is called everytime the page is loaded');
_getHomePageData();
}
Future<String> _getHomePageData() async{
var responseLatestArtwork = await http.get(Uri.encodeFull(homePageLatestArtworkApi));
var responseAllArtworks = await http.get(Uri.encodeFull(homePageAllArtworkApi));
var responseAllEvents = await http.get(Uri.encodeFull(homePageAllEventsApi));
var responseAllVenues = await http.get(Uri.encodeFull(homePageAllVenuesApi));
setState(() {
//latest artwork
var convertDataToJsonLatestArtwork = json.decode(responseLatestArtwork.body);
homePageLatestArtwork = convertDataToJsonLatestArtwork;
//All Artworks
var convertDataToJsonAllArtworks = json.decode(responseAllArtworks.body);
homePageAllArtworks = convertDataToJsonAllArtworks;
// All Events
var convertDataToJsonAllEvents = json.decode(responseAllEvents.body);
homePageAllEvents = convertDataToJsonAllEvents;
//All venues
var convertDataToJson = json.decode(responseAllVenues.body);
homePageAllVenues = convertDataToJson;
if(homePageLatestArtwork != null && homePageAllArtworks != null && homePageAllEvents != null && homePageAllVenues != null){
allDataLoaded = true;
}
// print('converted data is here');
//print(homePageLatestArtwork);
//print('the title is here :');
//print(homePageLatestArtwork[0]['title']['rendered']);
//print(homePageLatestArtwork[0]['_embedded']['wp:featuredmedia'][0]['source_url']);
});
#override
Widget build(BuildContext context) {
if(allDataLoaded){ //wait for the data to load and show spinning loader untill data is completely loaded
return Scaffold(
// body: Text('title'),
// body: Text("title: ${homePageLatestArtwork[0]['title']['rendered']} "),
body: Container(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Expanded(
Container(
height: 200.0,
child: Image.network(homePageLatestArtwork[0]['_embedded']['wp:featuredmedia'][0]['source_url'],
fit: BoxFit.fill,
width: double.maxFinite,),
),
Container(
padding: EdgeInsets.only(left:15.0,right:15.0,top:10.0),
child: Text(homePageLatestArtwork[0]['title']['rendered'],textAlign: TextAlign.left,
style: new TextStyle(
fontSize: 20.0,
fontFamily: 'Montserrat-Regular',
color: Color(0XFF212C3A),
fontWeight: FontWeight.bold
),),
),
])
),
),
);
}else{
return new Center(
child: new CircularProgressIndicator(
valueColor: new AlwaysStoppedAnimation<Color> .
(Theme.of(context).primaryColor),
),
);
}
} //end of _HOMEPAGESTATE
I would like to not load every single time the home page is viewed, or let's say I just want to fetch the api data once when the app starts and let users manually pull down to refresh the data.
Method 1:
If you are just talking about data then you should have a singleton object that you can init once in initState();
class _HomePageData {
var allDataLoaded = false;
String contents = "";
Future<String> _getHomePageData() async {
// Assuming contents is the data model and you load the data into contents
this.contents = "Loaded";
this.allDataLoaded = true;
return this.contents;
}
}
final _HomePageData data = _HomePageData();
class _HomePageState extends State<HomePage> {
String contents;
#override
void initState(){
super.initState();
if (!data.allDataLoaded) {
data._getHomePageData().then((contents) {
setState(() {
this.contents = contents;
})
});
} else {
this.contents = data.contents;
}
}
}
Method 2:
Previously I was dealing with tabs that constantly reload every time I move to another tab. Flutter actively remove state when they are not attached to any active widget tree. The idea is to have the widget in a Stack, placed into Offstage & TickerMode to control visibility and animation.
class MyTabState extends State<MyTabPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: MyBottomBar(
onTab: _onTab,
currentIndex: _currentIndex,
),
body: Stack(
children: List.generate(3, (index) {
return Offstage(
offstage: _currentIndex != index,
child: TickerMode(
enabled: _currentIndex == index,
child: getChild(index),
),
);
}, growable: false),
),
);
}
}
I hope I am not enough late. Anyhow if anyone else get the same situation not to refresh again and again you can add the following line at the end of your state. For example
with AutomaticKeepAliveClientMixin
It should be like this:
class _HomeScreenState extends State<HomeScreen>
with AutomaticKeepAliveClientMixin
then you can add an override
#override
bool get wantKeepAlive => true;
You are ready to rock.
I need help creating the architecture for my application. I am using Flutter and scoped_model to maintain state.
It's an application that has a login, that displays news in one part of the application, and shows a photo gallery among others. I would like to split this entire thing into separate Models. LoginModel that holds Login state (like username, token, name etc.). NewsModel that contains news retrieved from the API. GalleryModel to hold names of photos etc. I am not sure if this is the best practice to maintain state using scoped_model.
For eg, what If a text box depends on both LoginModel and NewsModel? I am not sure, but I guess it's not possible to retrieve state from two separate models.
Also, the main reason I am maintaining separate Models to hold state is that I don't want the Login part of the app to get refreshed when I bring news. I guess that's how it goes when I put the entire state in a single model.
The scoped_model library is designed to work with multiple models in play at the same time. That's part of the reason that ScopedModel and ScopedModelDescendant are generics and have a type parameter. You can define multiple models near the top of your Widget tree using ScopedModel<LoginModel> and ScopedModel<NewsModel> and then consume those models lower in the tree using ScopedModelDescendant<LoginModel> and ScopedModelDescendant<NewsModel>. The descendants will go looking for the appropriate model based on their type parameter.
I knocked together a quick example. Here are the models:
class ModelA extends Model {
int count = 1;
void inc() {
count++;
notifyListeners();
}
}
class ModelB extends Model {
int count = 1;
void inc() {
count++;
notifyListeners();
}
}
And here's what I'm displaying in the app:
ScopedModel<ModelA>(
model: ModelA(),
child: ScopedModel<ModelB>(
model: ModelB(),
child: ScopedModelDescendant<ModelA>(
builder: (_, __, a) => ScopedModelDescendant<ModelB>(
builder: (_, __, b) {
return Center(
child: Column(
children: [
GestureDetector(
onTap: () => a.inc(),
child: Text(a.count.toString()),
),
SizedBox(height:100.0),
GestureDetector(
onTap: () => b.inc(),
child: Text(b.count.toString()),
),
],
),
);
},
),
),
),
)
It seems to be working just fine. A non-nested approach works as well:
ScopedModel<ModelA>(
model: ModelA(),
child: ScopedModel<ModelB>(
model: ModelB(),
child: Column(
children: [
ScopedModelDescendant<ModelA>(
builder: (_, __, model) => GestureDetector(
onTap: () => model.inc(),
child: Text(model.count.toString()),
),
),
SizedBox(height: 100.0),
ScopedModelDescendant<ModelB>(
builder: (_, __, model) => GestureDetector(
onTap: () => model.inc(),
child: Text(model.count.toString()),
),
),
],
),
),
)
I wanted to give you a simple example on ScopedModel.
pubspec.yaml file must include :-
dependencies:
scoped_model: ^1.0.1
then,
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
void main() => runApp(MyApp()); //main method
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(), //new class MyHomePage
);
}
}
//-----------------------------------CounterModel [used by ScopedModel]
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
//-----------------------------------ends
//-----------------------------------MyHomePage class
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return ScopedModel( // ScopedModel used on top of the widget tree [it is wrapping up scaffold]
model: CounterModel(), // providing the CounterModel class as model
child: Scaffold(
appBar: AppBar(),
body: Container(
child: ScopedModelDescendant<CounterModel>( // ScopedModelDescendant accessing the data through ScopedModel
builder: (context, _, model) => Text("${model._counter}"), // fetching data from model without thinking of managing any state.
),
),
floatingActionButton: ScopedModelDescendant<CounterModel>(
builder: (context, _, model) => FloatingActionButton(
onPressed: model.increment, // calling function of model to increment counter
),
),
),
);
}
}
//-----------------------------------ends