I have a bloc which emits states that contain some progress value (0-100%). This value is to be displayed with LinearProgressIndicator. Updates may occur in chunks, meaning progress can jump from, say, 0% to 30% in a single step.
Below is a simple snippet to reproduce this behavior:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class StartProgressEvent {}
class ProgressState {
final double progress;
ProgressState(this.progress);
}
class ProgressBloc extends Bloc<StartProgressEvent, ProgressState> {
ProgressBloc() : super(ProgressState(0)) {
on<StartProgressEvent>(_startProgress);
}
void _startProgress(
StartProgressEvent event,
Emitter<ProgressState> emit,
) async {
emit(ProgressState(0.1));
await Future.delayed(const Duration(seconds: 3));
emit(ProgressState(0.4));
await Future.delayed(const Duration(seconds: 3));
emit(ProgressState(0.7));
await Future.delayed(const Duration(seconds: 3));
emit(ProgressState(1.0));
}
}
void main() {
runApp(const DemoApp());
}
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: BlocProvider<ProgressBloc>(
create: (context) => ProgressBloc()..add(StartProgressEvent()),
child: BlocBuilder<ProgressBloc, ProgressState>(
builder: (context, state) => LinearProgressIndicator(value: state.progress),
),
),
),
),
);
}
}
This snippet shows the following indicator:
Instead of updating instantly, I want my indicator to animate ongoing changes, i.e. to update smoothly from its previous state to the current one.
I found this answer suggesting that we use TweenAnimationBuilder to animate LinearProgressIndicator but it implies that we know its current value which we don't.
In a broader sense this question is not limited to progress indicator. I believe it can be framed this way: how can we animate between two consecutive "states" of a widget (either stateless or stateful) within bloc architecture?
You can try AnimatedFractionallySizedBox with your duration instead of LinearProgressIndicator
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocProvider<ProgressBloc>(
create: (context) => ProgressBloc()..add(StartProgressEvent()),
child: BlocBuilder<ProgressBloc, ProgressState>(
builder: (context, state) => AnimatedFractionallySizedBox(
duration: Duration(seconds: 3),
alignment: Alignment.centerLeft,
widthFactor: state.progress,
child: Container(
alignment: Alignment.centerLeft,
width: double.infinity,
height: 10,
color: Colors.blue,
),
)
// LinearProgressIndicator(value: state.progress),
),
),
],
),
),
);
}
}
Related
Hello i need help with my Provider that should trigger changes.
I am using Provider for my State management.
Ill inserted the Provider as followed:
main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => ThemeProvider()..initialize(),
),
ChangeNotifierProvider(
create: (_) => HeadingProvider(),
),
],
child: const Master(),
);
}
}
My Provider looks like this:
class HeadingProvider extends ChangeNotifier {
List<Widget> myWidgetList = [
const Text("hello"),
];
addHeading() {
myWidgetList.add(const Text("hello 2"));
notifyListeners();
}
}
I now have two different Widgets, one that triggers the function addHeading() and one that shows the list of the Provider. When I now call the function addHeading() the element Text("hello 2") gets created but does not trigger show in the UI. I have to manually hotreload to see changes.
Provider is referred like this in the two widgets
final headingController = Provider.of<HeadingProvider>(context);
Widget that triggers the function.
DragItemWidget(
onTap: () {
headingController.addHeading();
},
Widget that displayes the list.
Container(
width: 390,
height: 300,
child: ListView(
children: headingController.myWidgetList
),
),
Can you help me how I can see the added element in the UI immediately? (Maybe I have to trigger setState ?)
Wrap you ListView into a Consumer so that it will be rebuilt when you call notifyListeners():
Container(
width: 390,
height: 300,
child: Consumer<HeadingProvider>(builder: (context, headingProvider, _) {
return ListView(
children: headingProvider.myWidgetList
);
}),
),
Well the issue is kinda simple, but it needs to be done on a specific way. First I have a Class extending "ChangeNotifier" this class will perform some async tasks, so while it is doing so there's a variable that indicates if the class is currently bussy or not, so far it works flawlessly.
Using Riverpod as state managment I instanciate said class and provide it along my widget tree, but there's one Widget that needs to display a dialog and inside this dialog it can execute async tasks from the Class that I've been passing around. It all works except for the fact that I would like to display a CircularProgressIndicator inside this dialog, and it doesn't seems to be reacting propperly to the state changes.
Here's a sample code to recreate the scenario:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
final dataProvider = ChangeNotifierProvider<Data>((_) => Data());
void main() {
runApp(ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'huh?',
theme: ThemeData(primarySwatch: Colors.blue),
home: FirstPage(),
);
}
}
class FirstPage extends HookWidget {
#override
Widget build(BuildContext context) {
final data = useProvider(dataProvider);
print('DATA STATE [source: FirstPage, data: ${data.loading}]');
return Scaffold(
body: Center(
child: Container(
width: 200,
height: 50,
child: ElevatedButton(
child: Text('show dialog'),
onPressed: () => showDialog(
context: context,
builder: (_) => Alert(data: data),
),
),
),
),
);
}
}
class Alert extends StatelessWidget {
const Alert({required this.data});
final Data data;
Widget build(BuildContext context) {
print('DATA STATE [source: Alert, data: ${data.loading}]');
return AlertDialog(
content: Container(
width: 500,
height: 500,
padding: EdgeInsets.symmetric(horizontal: 100, vertical: 200),
child: ElevatedButton(
child: data.loading ? CircularProgressIndicator(color: Colors.white) : Text('click here'),
onPressed: () async => await data.randomTask(),
),
),
);
}
}
class Data extends ChangeNotifier {
Data({
this.loading = false,
});
bool loading;
Future<void> randomTask() async {
print('Actually waiting 3 seconds..');
_update(loading: true);
await Future.delayed(Duration(seconds: 3));
print('Waiting done.');
_update(loading: false);
}
void _update({bool? loading}) {
this.loading = loading ?? this.loading;
notifyListeners();
}
}
Notice the prints I've placed, because of them if you run the app you'll see outputs on the console like:
DATA STATE [source: FirstPage, data: false]
DATA STATE [source: Alert, data: false]
Actually waiting 3 seconds..
DATA STATE [source: FirstPage, data: true]
Waiting done.
DATA STATE [source: FirstPage, data: false]
Which means that the state is actually changing, and everything is working fine, except for the dialog that seems to be static.
I already tried adding a "loading" bool as part of the "Alert" widget, and letting it manage its own state, and it works, but the code is not as clean as I would like to, because the Class "Data" is supposed to manage this kind of stuff.
Is there anything that can be done?
Thankyou in advance!
Adding StatefulBulider do the trick
class Alert extends StatelessWidget {
const Alert({required this.data});
final Data data;
Widget build(BuildContext context) {
print('DATA STATE [source: Alert, data: ${data.loading}]');
return AlertDialog(
content: StatefulBuilder(builder: (context, setState) {
return Container(
width: 500,
height: 500,
padding: EdgeInsets.symmetric(horizontal: 100, vertical: 200),
child: ElevatedButton(
child: data.loading
? CircularProgressIndicator(color: Colors.white)
: Text('click here'),
onPressed: () async => await data.randomTask(),
),
);
}),
);
}
}
so I'm trying to implement a splash screen where for two reasons.
the time given to the splash screen will be used to load all the data
For beautification
I'm using flutter_spinkit
So here's my code:
class _SplashScreenState extends State<SplashScreen> {
void initState() {
super.initState();
navigateToHomeScreen();
}
Future navigateToHomeScreen() async {
return Timer(
const Duration(milliseconds: 4000),
() {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (BuildContext context) => App())); ---> doesn't go to new screen
},
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Color(0xff75c760),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildAppName(),
SizedBox(height: 20),
_buildSpinner(),
],
),
),
);
}
Widget _buildAppName() {
return Text(
"MasterChef",
style: GoogleFonts.robotoMono(
color: Colors.black,
fontStyle: FontStyle.normal,
fontSize: 30,
fontWeight: FontWeight.bold,
),
);
}
Widget _buildSpinner() {
return SpinKitDoubleBounce(
color: Colors.white,
);
}
}
and here's the App() from app.dart:
class _AppState extends State<App> {
//Contains the simple drawer with switch case for navigation
//Taken directly from the flutter docs
}
The basic idea is to load the splash screen for 4000 milliseconds in which the app will load all the necessary data and then navigate to App() which contains the navigation routes and all. But for some reason I'm getting Unhandled Exception: Navigator operation requested with a context that does not include a Navigator.
I cannot be certain to what the cause is as cannot see your entire app structure but I am guessing the cause is that you do not have a MaterialApp() at the root of your app.
If this doesn't work please update your question to contain more code of how you get the splash screen app to be displayed.
This is because you used return Timer() so remove the return keyword so, try changing your code to:
Future navigateToHomeScreen() async {
Timer(
const Duration(milliseconds: 4000),
() {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (BuildContext context) => App()));
},
);
}
Alternatively, you can use Future instead of Timer
Future navigateToHomeScreen() async {
Future.delayed(
const Duration(milliseconds: 4000),
() {
Navigator.pushReplacement(context,
MaterialPageRoute(builder: (BuildContext context) => App()));
},
);
}
I'm trying to implement Provider state management on counter application to understand Provider's functionality better. I have added two buttons with respect to two different text widget. So, now whenever I click any of the two widget both the Text widgets get update and give same value. I want both the widgets independent to each other.
I have used ScopedModel already and got the desire result but now I want to try with provider.
Image Link : https://i.stack.imgur.com/ma3tR.png
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print("====Home Page Rebuilt====");
return Scaffold(
appBar: AppBar(
title: Text("HomePage"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment:CrossAxisAlignment.center,
children: [
Consumer<CounterModel>(
builder: (context, value, child) {
return CustomWidget(
number: value.count.toString(),
);
},
),
Consumer<CounterModel>(
builder: (context, value, child) {
return CustomWidget(
number: value.count.toString(),
);
},
),
],
)),
);
}
}
class CustomWidget extends StatelessWidget {
final String number;
const CustomWidget({Key key, this.number}) : super(key: key);
#override
Widget build(BuildContext context) {
print("====Number Page Rebuilt====");
return ButtonBar(
alignment: MainAxisAlignment.center,
children: [
Consumer<CounterModel>(
builder: (context, value, child) {
return Text(
value.count.toString(),
style: Theme.of(context).textTheme.headline3,
);
},
),
FlatButton(
color: Colors.blue,
onPressed: () =>
Provider.of<CounterModel>(context, listen: false).increment(),
child: Text("Click"),
),
],
);
}
}
If you want them independent from each other, then you need to differentiate them somehow. I have a bit of a different style to implement the Provider and it hasn't failed me yet. Here is a complete example.
You should adapt your implementation to something like this:
Define your provider class that extends ChangeNotifier in a CounterProvider.dart file
import 'package:flutter/material.dart';
class CounterProvider extends ChangeNotifier {
/// You can either set an initial value here or use a UserProvider object
/// and call the setter to give it an initial value somewhere in your app, like in main.dart
int _counter = 0; // This will set the initial value of the counter to 0
int get counter => _counter;
set counter(int newValue) {
_counter = newValue;
/// MAKE SURE YOU NOTIFY LISTENERS IN YOUR SETTER
notifyListeners();
}
}
Wrap your app with a Provider Widget like so
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// don't forget to import it here too
import 'package:app/CounterProvider.dart';
void main() {
runApp(
MaterialApp(
initialRoute: '/root',
routes: {
'/root': (context) => MyApp(),
},
title: "Your App Title",
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
/// Makes data available to everything below it in the Widget tree
/// Basically the entire app.
ChangeNotifierProvider<CounterProvider>.value(value: CounterProvider()),
],
child: MaterialApp(
home: HomeScreen(),
),
);
}
}
Access and update data anywhere in the app
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// MAKE SURE TO IMPORT THE CounterProvider.dart file
import 'package:app/CounterProvider.dart';
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
CounterProvider counterProvider;
#override
Widget build(BuildContext context) {
/// LISTEN TO THE CHANGES / UPDATES IN THE PROVIDER
counterProvider = Provider.of<CounterProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text("HomePage"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment:CrossAxisAlignment.center,
children: [
_showCounterButton(1),
_showCounterButton(2),
],
),
),
);
}
Widget _showCounterButton(int i) {
return ButtonBar(
alignment: MainAxisAlignment.center,
children: [
Text(
i == 1
? counterProvider.counter1.toString()
: counterProvider.counter2.toString(),
style: Theme.of(context).textTheme.headline3,
),
FlatButton(
color: Colors.blue,
onPressed: () {
/// UPDATE DATA IN THE PROVIDER. BECAUSE YOU're USING THE SETTER HERE,
/// THE LISTENERS WILL BE NOTIFIED AND UPDATE ACCORDINGLY
/// you can do this in any other file anywhere in the Widget tree, as long as
/// it it beneath the main.dart file where you defined the MultiProvider
i == 1
? counterProvider.counter1 += 1
: counterProvider.counter2 += 1;
setState(() {});
},
child: Text("Click"),
),
],
);
}
}
If you want, you can change the implementation a bit. If you have multiple counters, for multiple widgets, then just create more variables in the CounterProvider.dart file with separate setters and getters for each counter. Then, to display/update them properly, just use a switch case inside the _showCounterButton() method and inside the onPressed: (){ switch case here, before setState((){}); }.
Hope this helps and gives you a better understanding of how Provider works.
I have this value of type list that I'll be using on a ListViewBuilder.
I have a condition that checks if the list has a length of 0 or more so that I can show an Empty State widget if there are no available items in the list and to show the ListViewBuilder if there are items in the list.
Running the logic actually show's that it works but my problem is that since the list object's length starts at 0 it always draws my "Empty State" widget first but then quickly draws the actual ListViewBuilder when my Future function finishes incrementing on my list. Though it works, the experience for the user to quickly see the "Empty State" even if there actually items on list is quite jarring.
Hoping you guys can help me on a way I can only show either state my list is on without having to pass through from the initial state of zero.
Below is a representation of what I am talking about, you can see that isolated my three states: NULL (White), ==0 (Blue), >=1 (Green).
What I'm trying to achieve is to just go to Green without showing Blue.
EDIT: ADDED SAMPLE CODE BELOW
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MainProvider extends ChangeNotifier {
List<String> list = <String>[];
MainProvider() {
initList();
}
initList() async {
// CODE BELOW IS TO SIMULATE MAKING DATABASE CALLS
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(milliseconds: 800), () {
addToList('String');
});
}
}
addToList(String string) {
list.add(string);
notifyListeners();
}
}
void main() => runApp(AppIndex());
class AppIndex extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: AppProvider(),
);
}
}
class AppProvider extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MainProvider>(
create: (context) => MainProvider(),
child: AppContent(),
);
}
}
class AppContent extends StatelessWidget {
#override
Widget build(BuildContext context) {
var mainProvider = Provider.of<MainProvider>(context);
return Scaffold(
appBar: AppBar(),
body: mainProvider.list == null
? Container(
color: Colors.red,
child: Center(
child: Center(
child: Text(
'NULL',
),
),
),
)
: mainProvider.list.length == 0
? Container(
color: Colors.blue,
child: Center(
child: Text(
mainProvider.list.length.toString(),
style: TextStyle(fontSize: 32.0),
),
),
)
: mainProvider.list.length >= 0
? Container(
color: Colors.green,
child: Center(
child: Text(
mainProvider.list.length.toString(),
style: TextStyle(fontSize: 32.0),
),
),
)
: Container(
color: Colors.red,
child: Center(
child: Text(
mainProvider.list.length.toString(),
style: TextStyle(fontSize: 32.0),
),
),
),
);
}
}
In my code, I am using my Future function to call out database items
to add to my list. In the sample code above, I intentionally added a
delay to simulate database call times.
Given your comment, I think something like this will do:
class _HomePageState extends State<HomePage> {
Future<List<Item>> _items;
#override
void initState() {
super.initState();
// here maybe you won't use the server side, but some local storage
_items = Web.fetchItems();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<List<Item>>(
future: _items,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (_items.isEmpty) {
return _MyCallToActionButtonLayout();
}
return Scaffold(
appBar: AppBar(
title: Text('My title'),
),
body: buildListView(snapshot.data)),
);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
} else {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
},
);
}