Flutter/Dart - Transparent Background to CircularProgressIndicator on a Loading Future? - flutter

At the moment I get a white background with a spinning CircularProgressIndicator when I swipe to a new route. The new route has a Future that fetches data from a HTTP Post. Instead I'd like the background of the original page to remain behind the spinner until the future completes and the transition happens. So how do I make the spinners' background transparent instead of white? Here's the code to the future which I assume is where the spinner gets triggered;
FutureBuilder<List<ReplyContent>> replyStage({String replyid, String replytitle}) {
return new FutureBuilder<List<ReplyContent>>(
future: downloadReplyJSON(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<ReplyContent> replycrafts = snapshot.data;
return StageBuilderVR(replycrafts, replytitle);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return CircularProgressIndicator();
},
);
}
And here's the code which swipes to the future;
onSwipeUp: () {
Navigator.of(context).push(_createRoute());
}
And the code for the PageRouteBuilder:
Route _createRoute() {
return PageRouteBuilder(
opaque: false,
pageBuilder: (context, animation, secondaryAnimation) => ReplyHome(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
var begin = Offset(0.0, 1.0);
var end = Offset.zero;
var curve = Curves.ease;
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
);
}

You can use showDialog to open a dialog which will open a transparent background with the AlertDialog, You can return your own stateful widget. Instead of streamBuilder just use future Builder.
try following code:
void uploadAndShowProgress() async {
await showDialog(
context: context,
builder: (context) {
return StreamBuilder(
stream: uploadFile(),
builder: (context, snapshot) {
if (snapshot.hasData) {
StorageTaskEvent event = snapshot.data;
final _snapshot = event.snapshot;
_progress = _snapshot.bytesTransferred / _snapshot.totalByteCount;
} else {
//* pop when there's no data or error...
Navigator.pop(context);
}
if (snapshot.hasError) {
SnackbarWidget.showSnackbar(
content: Text(snapshot.error.toString()), context: context);
}
return AlertDialogWidget(progress: _progress);
},
);
},
);
}

this function pushes route with transparent background
onPressed: () {
Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
pageBuilder: (_, __, ___) {
return MyTransperentRoute();
},
),
);
}
so in your CircularProgressIndicator page you can change background color of the root Widget like color: Colors.transperent or a plain Container without any color set will achieve the effect you need
class MyTrnsperentPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: const Center(
child: CircularProgressIndicator(),
),
);
}
}
see the working code on dartpad here

Stack(
children: <Widget>[
Opacity(
opacity: _isLoading ? 1.0 : 0,
child: CircularProgressIndicator(),
),
],
);

In the end, I created a dummy Page that graphically mirrored the previous page with all the graphic elements of the previous page and none of the code. I put that in place of the CircularProgressIndicator. I don't know if it's ideal, but it seems to work well.
FutureBuilder<List<ReplyContent>> replyStage({String replyid, String replytitle}) {
return new FutureBuilder<List<ReplyContent>>(
future: downloadReplyJSON(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<ReplyContent> replycrafts = snapshot.data;
return StageBuilderVR(replycrafts, replytitle);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return DummyPage();
},
);
}

Related

Cannot share a file as it need to wait a future

Share() must display a modal in order to let the user wait in front of a circular progress indicator while I am loading the video file URL.
My code is as below, but I am puzzled about how to architecture: I need to trigger the sharing only once the snapshot.hasData.
How can that be done?
Btw, I use share_plus
Future<void> share(BuildContext context) async {
showModalBottomSheet(
context: context,
builder: (context) {
return FutureBuilder(
future: Video.videoUrl(videoUrl!),
builder: (context, snapshot) {
final file = XFile(snapshot.data!);
Share.shareXFiles([file],
text: "Don't miss this out! Only on Shokaze");
return SizedBox(
height: 200,
child: Center(
child: !snapshot.hasData
? Column(children: [
Text("Preparing sharing…"),
const CircularProgressIndicator(),
])
: Text("Sharing…")));
});
});
}
You should refactor the FutureBuilder using if/else conditions:
if (snapshot.hasData) {
Share.share(snapshot.data!);
return Text(snapshot.data.toString());
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
Future<void> share(BuildContext context) async {
showModalBottomSheet(
context: context,
builder: (context) {
return FutureBuilder(
future: myFuture(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
Share.share(snapshot.data!);
return Text(snapshot.data.toString());
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
},
);
},
);
}

How can I update state notifier(riverpod) from Go router state.params

The pagebuilder in GoRoute has the the only place I can grab the state.params, I want to update my StateNotifier when the route changes if it is different.
final LibraryRoutes = Provider<RouteBase>((ref) {
return ShellRoute(
navigatorKey: _LibraryKey,
builder: (context, state, child) {
return LibraryHomePage(
child: child,
);
},
routes: [
//dashboard
GoRoute(
path: "/library/:LibraryKey/Dashboard",
pageBuilder: (context, state) {
final String passedValue = state.params['LibraryKey']!;
final newLibrary = LibraryReadDto(LibraryKey: passedValue);
//this line error's out because its during lifecycle method
ref.read(AsyncLibraryProvidor.notifier).updateLibrary(newLibrary);
final AsyncLibraryNotifier = ref.watch(AsyncLibraryProvidor);
return AsyncLibraryNotifier.when(data: (data) {
return NoTransitionPage(
child: Text("dashboard"),
);
}, error: (_, __) {
return NoTransitionPage(
child: const Text("An error occurred"),
);
}, loading: () {
return NoTransitionPage(
child: CircularProgressIndicator(),
);
});
}),
]);
});
I've managed to put the update in a future to get around the problem is there a more elegant solution as the library is used in many different places.
GoRoute(
path: "/library/:LibraryKey/Dashboard",
pageBuilder: (context, state) {
if (ref.read(LibraryProvider) == null) {
final String passedValue = state.params['LibraryKey']!;
try {
//can't update during widget tree delay by 150 micoseconds so we can update after future
Future.delayed(
const Duration(microseconds: 150),
() {
final newLibrary = LibraryReadDto(LibraryKey: passedValue);
ref.read(LibraryProvider.notifier).updateLibrary(newLibrary);
},
);
} on Exception catch (ex) {
print(ex.toString());
}
}
return NoTransitionPage(
child: Text("dashboard"),
);
}),
Right now It's not possible to use await within the page Builder,
Write Some Logic in your page side to prefetch the values before the page renders.

Flutter bloc - do not call the api every time (use the same State in different views)

I'm using flutter bloc pattern in my flutter app. I have bottom navigation bar with several tabs in one page. Two of them are using the same api call (the same State). When the user taps on 1 of them I call the api to get the data, but if the user taps on the other tab I want to get the data without calling the api again. How I can do that?
In my main page (dashboard) I have BlocBuilder to change the tabs and I create the Dashboard cubit in it
class DashboardPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<TabsBloc, AppTab>(
builder: (BuildContext context, AppTab activeTab) {
return Scaffold(
appBar: AppBar(
title: Text(DashboardHelpers.getTabLabel(activeTab)),
),
body: RepositoryProvider(
create: (BuildContext context) => DashboardRepository(),
child: BlocProvider<DashboardCubit>(
create: (BuildContext context) => DashboardCubit(
dashboardRepository: context.read<DashboardRepository>(),
authBloc: context.read<AuthBloc>(),
),
child: DashboardHelpers.getTabContent(activeTab),
),
),
bottomNavigationBar: TabSelector(
activeTab: activeTab,
onTabSelected: (tab) =>
BlocProvider.of<TabsBloc>(context).add(TabUpdated(tab))),
);
},
);
}
}
The tabs are View that are loaded as child. One of the views is View1. When I get the data I loaded in ContentView1
class View1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
BlocProvider.of<DashboardCubit>(context)..getDashboardDevices();
return BlocConsumer<DashboardCubit, DashboardState>(
listener: (BuildContext context, DashboardState state) {
if (state is DashboardError) {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Text(state.message),
),
);
}
},
builder: (BuildContext context, DashboardState state) {
if (state is DevicesLoaded) {
return ContentView1(data: state.data);
} else if (state is DashboardLoading) {
return LoadingWidget();
} else if (state is DashboardError) {
return Container(
child: Center(
child: Text(state.message),
),
);
} else {
return Container();
}
},
);
}
}
and the View2 is almost the same. The data is absolutely the same and it is loaded in ContentView2 but it is a completly different widget than ContentView1
class View2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
BlocProvider.of<DashboardCubit>(context)..getDashboardDevices();
return BlocConsumer<DashboardCubit, DashboardState>(
listener: (BuildContext context, DashboardState state) {
if (state is DashboardError) {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Text(state.message),
),
);
}
},
builder: (BuildContext context, DashboardState state) {
if (state is DevicesLoaded) {
return ContentView2(data: state.data);
} else if (state is DashboardLoading) {
return LoadingWidget();
} else if (state is DashboardError) {
return Container(
child: Center(
child: Text(state.message),
),
);
} else {
return Container();
}
},
);
}
}
The problem is that these two VIEWs are showing different data that comes from the same API endpoint.
How can I load the already gotten data when the user tabs from View1 to View2 without calling the API again.
Thanks!
You should be calling getDashboardDevices() only once, for that you could create a DashboardInitialState, when user clicks one of tabs, if the state is DashboardInitialState you run getDashboardDevices() and not always when the view is building. This way you will load data only once when one of the views is built and both of them will use the same data on loaded state.
There is View1 as example, try that with both views:
class View1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocConsumer<DashboardCubit, DashboardState>(
listener: (BuildContext context, DashboardState state) {
if (state is DashboardError) {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Text(state.message),
),
);
}
},
builder: (BuildContext context, DashboardState state) {
if(state is DashboardInitialState) {
BlocProvider.of<DashboardCubit>(context)..getDashboardDevices();
return LoadingWidget();
} else if (state is DevicesLoaded) {
return ContentView1(data: state.data);
} else if (state is DashboardLoading) {
return LoadingWidget();
} else if (state is DashboardError) {
return Container(
child: Center(
child: Text(state.message),
),
);
} else {
return Container();
}
},
);
}
}

Close loading dialog in FutureBuilder

I am using Futurebuilder in flutter and having issue while closing the showDialog
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: Size( 50.0),
body: FutureBuilder(
future: widget.getAutoCompleteData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
Navigator.of(context).pop();
} else if (snapshot.hasError) {
} else {
LoadingDialog.showLoadingDialog(context, _scaffoldKey);
}
return Center(
child: Container(child: Text("sds"),),
);
}));
}
Getting below error when screen loads
package:flutter/src/widgets/navigator.dart': Failed assertion: line 5013 pos 12: '!_debugLocked': is not true
Change this
FutureBuilder(
future: widget.getAutoCompleteData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
Navigator.of(context).pop();
} else if (snapshot.hasError) { //here this is empty
} else {//remove else statement
LoadingDialog.showLoadingDialog(context, _scaffoldKey);
}
return Center(
child: Container(child: Text("sds"),),
);
})
To This
FutureBuilder<List<Post>>(
future: _dataFetcher.getPosts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return LoadingDialog.showLoadingDialog(context, _scaffoldKey);
}
return Center(child: Text('${snapshot.data![0].title}'));
},
)
It may be caused by the re-entrant of the Navigator (you can check the answer here: Error thrown on navigator pop until : “!_debugLocked': is not true.”
)
Or, maybe you don't want to use FutureBuilder. The FutureBuilder is meant to stay in the same widget/page and show different screens when future data is not ready. If you want to push a loading dialog and close it when data is ready, you can just simply use a Future function
Future pressTheButton(context) async {
LoadingDialog.showLoadingDialog(context, _scaffoldKey); // showDialog here
final data = await getAutoCompleteData(); // await the data
Navigator.of(context).pop(); // pop the loading dialog
// return your data or error
// or rebuild the current widget with the data
}

FutureBuilder only works in Debug

I have a FutureBuilder with a ListView to display custom items (Widgets) with values which are read from .txt files.
The problem is that these items are only displayed if I launch the app in Debug-mode or run-mode. When I try to open the app with the AppLauncher (like a "normal" user would do it) the listView is empty. I tried this on an AVD and on a "real" device.
the Future "listFuture" is used to read the values from the files and return a list of Widgets
class Home extends StatefulWidget {
final Future listFuture = setupList();
#protected
#mustCallSuper
void initState() {
print("init complete");
}
#override
State<StatefulWidget> createState() {
return HomeState();
}
}
If the FutureBuilder gets the data correctly a listView with the list of my widgets should be displayed
child: FutureBuilder<List<SubListItem>>(
future: widget.listFuture,
// ignore: missing_return
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text("None");
case ConnectionState.waiting:
return new Text("loading");
default:
if (snapshot.hasError) {
print("Error");
return Center(child: (Text("No data")));
} else {
return subListView(context, snapshot);
}
}
},
),
Widget subListView(BuildContext context, AsyncSnapshot snapshot) {
List<Widget> items = snapshot.data;
//This ScrollConfiguration is used to remove any animations while scrolling
return ScrollConfiguration(
behavior: CustomScrollBehavior(),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 4),
child: new ListView.builder(
itemCount: items.length,
itemBuilder: (BuildContext context, int index) {
return Column(
children: <Widget>[items[index]],
);
},
),
),
);
}
Thanks for helping!
Ok, I solved the problem. You just have to call "setState" when your Widget is built.
#protected
#mustCallSuper
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
//This setState is necessary because it refreshes the listView
setState(() {});
});
}
It's looks like a async data issue, try these changes:
Remove listFuture from your StatefulWidget.
Add the listFuture var inside your State.
Move the setupList() method inside your State.
And finally call directly like this:
child: FutureBuilder<List<SubListItem>>(
future: setupList(),
// ignore: missing_return
builder: (BuildContext context, AsyncSnapshot snapshot) {
if(!snapshot.hasData) {
return new Text("loading");
}
else if (snapshot.hasError) {
print("Error");
return Center(child: (Text("No data")));
} else {
return subListView(context, snapshot);
}
}
},
),