Setting video path before initializing video controller - flutter

So, I'm trying to use flutter's example to test a video, but I want to provide a file path that is saved in the persistent storage. My problem is that I can't wrap my head around on how to do that.
Here's my code: https://dartpad.dev/6930fc8c208c9bd1c00ae34303365e48
Future<String> getVideo() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var videoid = prefs.getString('fileview');
return videoid;
}
#override
void initState() {
getVideo();
_controller = VideoPlayerController.file(File(getVideo()));
// Initialize the controller and store the Future for later use.
_initializeVideoPlayerFuture = _controller.initialize();
// Use the controller to loop the video.
_controller.setLooping(true);
super.initState();
}
}
So I can't set getVideo() to File because it's a future in initstate.

You can write another async function for initialising your controller and listen that future for building your UI.
Future initPlayer() async {
var filePath = await getVideo();
_controller = VideoPlayerController.file(File(filePath));
_initializeVideoPlayerFuture = _controller.initialize();
_controller.setLooping(true);
return _initializeVideoPlayerFuture;
}
You have to write another function to handle the playing state, because the player will be null when the build method will run for the first time.
bool get isVideoPlaying {
return _controller?.value?.isPlaying != null && _controller.value.isPlaying;
}
Finally, modify your build method like:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Butterfly Video'),
),
body: FutureBuilder(
future: initPlayer(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
if (isVideoPlaying) {
_controller?.pause();
} else {
_controller?.play();
}
});
},
child: Icon(
isVideoPlaying ? Icons.pause : Icons.play_arrow,
),
),
);
}

Related

Flutter - how to update screen with latest api response

I want to update the screen whenever I call the API. Right now I have the following
Future<String> getData() async {
var response = await http.get(
Uri.parse('https://www.api_endpoint.com'),
headers: {
'Accept':'application/json'
}
);
Timer.periodic(Duration(microseconds: 1000), (_) {
this.setState(() {
data = json.decode(response.body);
print(data); //I can see this in the console/logcat
});
});
}
#override
void initState() {
this.getData();
}
from the line above print(data); I can see the latest api responses in console/logcat but the screen doesn't update with the new values. I can't get my head around why the latest responses aren't shown on screen when this.setState() is called every second with the Timer... all feedback is welcome. Thanks
Future executes once and returns just one result. initState() executed when creating a widget, this is also usually once. For your tasks it is better to use Streams, my solution is not the best in terms of architecture, but as an example it works.
//We create a stream that will constantly read api data
Stream<String> remoteApi = (() async* {
const url = "http://jsonplaceholder.typicode.com/todos/1";
//Infinite loop is not good, but I have a simple example
while (true) {
try {
var response = await Dio().get(url);
if (response.statusCode == 200) {
//remote api data does not change, so i will add a timestamp
yield response.data.toString() +
DateTime.now().millisecondsSinceEpoch.toString();
}
//Pause of 1 second after each request
await Future.delayed(const Duration(seconds: 1));
} catch (e) {
print(e);
}
}
})();
//On the screen we are waiting for data and display it on the screen
// A new piece of data will refresh the screen
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: StreamBuilder<String>(
stream: remoteApi,
builder: (
BuildContext context,
AsyncSnapshot<String> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.connectionState == ConnectionState.active ||
snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Center(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Text(
snapshot.data.toString(),
textAlign: TextAlign.center,
),
),
);
} else {
return const Center(child: Text('Empty data'));
}
} else {
return Center(child: Text('State: ${snapshot.connectionState}'));
}
},
),
);
}
Or simplest solution
Future<String> remoteApi() async {
try {
const url = "http://jsonplaceholder.typicode.com/todos/1";
var response = await Dio().get(url);
if (response.statusCode == 200) {
return response.data.toString() +
DateTime.now().millisecondsSinceEpoch.toString();
} else {
throw ("Error happens");
}
} catch (e) {
throw ("Error happens");
}
}
var displayValue = "Empty data";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(child: Text(displayValue)),
Center(
child: ElevatedButton.icon(
onPressed: () async {
displayValue = await remoteApi();
setState(() {});
},
label: const Text('Get API'),
icon: const Icon(Icons.download),
),
)
],
),
));
}
Ah, you don't actually call your API every timer tick, you just decode the same body from the first call.
If you want to call your API periodically, you need to move the actual http.get call inside the timer method.
Got it using the answer found here... moved the Timer that called this.setState() to the initState method
#override
void initState() {
this.getData();
_everySecond = Timer.periodic(Duration(seconds: 5), (Timer t) {
setState(() {
getData();
});
});
}
Once I searched for how to update the state, change state, etc. found the solution quickly...

How to show loading spinner in GetBuilder

In FutureBuilder when working with an API you can easily show loading spinner when data is not yet available with this code,
if(snapshot.connectionState == ConnectionState.waiting){
return Center(
child: CircularProgressIndicator(),
);
}
how do I do same for GetBuilder when using getx as state management library?
Here's a basic example of re-building based on the value of an isLoading bool. I'm just changing the value of a String but this should give you the idea of doing a proper API call in a GetX function and displaying an indicator. While I typically default to using GetBuilder whenever possible, showing loading indicators I generally just use Obx so I don't have to call update() twice.
class TestController extends GetxController {
bool isLoading = false;
String data = '';
Future<void> fetchData() async {
isLoading = true;
update(); // triggers the GetBuilder rebuild
await Future.delayed(
const Duration(seconds: 2),
() => data = 'Data Loaded',
);
isLoading = false;
update();
}
}
You can test this by throwing this in a Column. Just make sure the controller is initialized first at some point with Get.put(TestController());
GetBuilder<TestController>(
builder: (controller) => controller.isLoading
? CircularProgressIndicator()
: Text(controller.data)),
ElevatedButton(
onPressed: () => controller.fetchData(),
child: Text('Fetch Data'),
),
If you don't want to have to manually call the function you can also lose the isLoading bool use a FutureBuilder but then just pass a Future function from a GetX class to keep that logic out of your UI.
Update
Here's an example using live dummy data of random Kanye quotes from
https://api.kanye.rest Copy the code below into your IDE and run it and it should make sense.
Basic ApiCaller class
class ApiCaller extends GetConnect {
final url = 'https://api.kanye.rest';
Future<String> fetchData() async {
final response = await httpClient.get(url);
return response.body['quote'] as String;
}
}
Updated TestController class
class TestController extends GetxController {
String data = 'no data';
bool isLoading = false;
Future<void> updateData() async {
_updateIsLoading(true);
final apiCaller = ApiCaller();
await Future.delayed(
const Duration(seconds: 1),
() => data = 'Data Loaded',
); // just to add more visible time with loading indicator
data = await apiCaller.fetchData();
_updateIsLoading(false);
}
void _updateIsLoading(bool currentStatus) {
isLoading = currentStatus;
update();
}
}
Example with GetBuilder and FutureBuilder
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
final controller = Get.put(TestController());
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FutureBuilder(
future: ApiCaller().fetchData(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('FutureBuilder: ${snapshot.data}');
} else {
return CircularProgressIndicator();
}
},
),
GetBuilder<TestController>(
builder: (_) => controller.isLoading
? CircularProgressIndicator()
: Text('GetBuilder: ${controller.data}'),
),
ElevatedButton(
onPressed: () => controller.updateData(),
child: Text('Update GetBuilder'),
),
],
),
),
),
);
}
}
Example with FutureBuilder with function from GetX class

Flutter - Stop FutureBuilder operation in between process

How do I cancel FutureBuilder operation when rebuilding the widget
Lets say I have a code like this... Every time i pressed the Floating button the widget rebuilds calling myFuture which waits five seconds and then the counter increments... Now I want that during that five seconds if I pressed the Floating button the current Future (which is still is delayed) should stop its operation and the new Future will be called...So at the end I should get a counter of 2 but instead I get 3...
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var counter = 0;
myFuture()async{
await Future.delayed(Duration(seconds:5));
counter++;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: FutureBuilder(
future: myFuture(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return Center(child: CircularProgressIndicator());
else return Text(counter.toString());
}
),
floatingActionButton: FloatingActionButton(
onPressed: ()=>setState(() {}),
child: Icon(Icons.add),
),
);
}
}
In order to cancel a Future, you can use the CancelableOperation from the async package.
It's implementation would look like the following :
Future<dynamic> executeCancelable(Future<dynamic> futureMethod) async {
operation?.cancel();
operation = CancelableOperation.fromFuture(futureMethod, onCancel: () {
print('Future stopped');
});
return operation.value;
}
Future<dynamic> futureMethod() async {
return Future.delayed(const Duration(milliseconds: 3000), () {
return counter++;
});
}
Which can be called with the following method :
executeCancelable(futureMethod())
Note that in this example, I'm using a Future.delayed wich can't "really" be cancelled as explained here.
This snippet would work well with an API query for example.
int counter = 0;
fiveSeconds() async {
await Future.delayed(Duration(seconds: 5));
}
twoSeconds() async {
await Future.delayed(Duration(seconds: 2));
}
bool _futureTime = true;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: FutureBuilder(
future: _futureTime ? fiveSeconds() : twoSeconds(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return Center(child: CircularProgressIndicator());
else counter++;
return Text(counter.toString());
}
),
floatingActionButton: FloatingActionButton(
onPressed: (){setState(() {
_futureTime = !_futureTime;
});
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Alternatively you can use a Stream and StreamBuilder instead of Future and FutureBuilder if that fits your use case.
void main() {
// keep a reference to your stream subscription
StreamSubscription<List> dataSub;
// convert the Future returned by getData() into a Stream
dataSub = getData().asStream().listen((List data) {
updateDisplay(data);
});
// user navigated away!
dataSub.cancel();
}
source: https://dart.academy/how_cancel_future/

Flutter FutureBuilder Does Not Wait When App Updates

Problem My FutureBuilder waits when app first runs but doesn't wait when app updates.
When my app finishes loading and I change to a different ToggleButton, the FutureBuilder starts to rerun immediately instead of waiting for getData() and it fully completes before getData() is finished and then when getData() is finally finished, FutureBuilder runs again.
This problem does not happen when the app first runs. When the app first runs, the FutureBuilder waits for getData() to complete before running.
I need FutureBuilder to wait for getData() to finish when a different button is pressed just like it does when the app first starts up.
Note: I removed as much unnecessary code as I could for readability. I can add more code if it will help.
Code:
class PriceScreenState extends State<PriceScreen> {
String selectedCurrency = 'USD';
String selectedGraphType = "1D";
var isSelectedGraph = <bool>[true, false, false, false, false, false];
getData() async {
isWaiting = true;
try {
Map graphData = await GraphData().getGraphData(
selectedCurrency: selectedCurrency,
selectedGraphType: selectedGraphType);
isWaiting = false;
setState(() {
graphValues = graphData;
});
} catch (e) {
print(e);
}
}
#override
void initState() {
super.initState();
futureData = getData();
}
#override
Widget build(BuildContext context) {
...(other code)...
ToggleButtons( ****************TOGGLEBUTTONS***********
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Text('1D'),
),
...(more Buttons)...
],
onPressed: (int index) {
setState(() {
for (int buttonIndex = 0;
buttonIndex < isSelectedGraph.length;
buttonIndex++) {
if (buttonIndex == index) {
isSelectedGraph[buttonIndex] = true;
selectedGraphType = graphType[buttonIndex];
} else {
isSelectedGraph[buttonIndex] = false;
}
}
});
getData();
},
isSelected: isSelectedGraph,
),
Expanded(
child: FutureBuilder( *************FUTUREBUILDER*********
future: futureData,
builder: (context, snapshot) {
if (graphValues.isEmpty) {
return new Container();
} else {
return Graph(graphValues);
}
}),
)
As you are using a FutureBuilder you don't need to call setState anymore. Here is a possible rework of your code:
Future<Map> futureData;
Future<Map> getData() async {
try {
Map graphData = await GraphData().getGraphData(
selectedCurrency: selectedCurrency,
selectedGraphType: selectedGraphType,
);
return graphData;
} catch (e) {
throw Exception(e);
}
}
#override
void initState() {
super.initState();
futureData = getData();
}
#override
Widget build(BuildContext context) {
// Only coding the FutureBuilder for the example
return FutureBuilder<Map>(
future: futureData,
builder: (context, snapshot) {
// Future is still loading
if (!snapshot.hasData)
return CircularProgressIndicator();
else if (snapshot.data.isEmpty)
return Container();
else
return Graph(snapshot.data);
},
);
}
For your FutureBuilder to work correctly you need to return a value in your getData and use the snapshot variable.

How to fix recursing HTTP request in FutureBuilder?

I'm creating an app that has a list inside a screen. What I want to do is whenever the app makes the HTTP request (getting the data), I want to show CircularProgressIndicator() on the screen. I tried to use a FutureBuilder to implement this, but the app recursively/continuously loading the data (when the ListView is set, the app load the data again and again). Here are some of my code:
FutureBuilder Widget
Widget _buildFuture(BuildContext context){
return FutureBuilder(
future: listenForBeers(),
builder: (context, snapshot) {
if(snapshot.connectionState == ConnectionState.done){
if(snapshot.hasError){
print('_buildFuture: Loading error');
return Center(
child: Text(
snapshot.error.toString(),
textAlign: TextAlign.center,
textScaleFactor: 1.3,
),
);
}
print('_buildFuture: Showing the Data');
return _buildBeers();
}
else{
print('_buildFuture: Loading the data');
return Center(
child: Column(
children: <Widget>[
SizedBox(height: 100),
CircularProgressIndicator()
],
),
);
}
}
);
}
initState() and listenForBeers() method
#override
void initState(){
super.initState();
listenForBeers();
}
Future listenForBeers() async {
final Stream<Beer> stream = await getBeers();
stream.listen((Beer beer) => setState(() => _beers.add(beer)));
}
getBeers() method
Future<Stream<Beer>> getBeers() async {
final String url = 'https://api.punkapi.com/v2/beers';
final client = new http.Client();
final streamedRest = await client.send(http.Request('get', Uri.parse(url)));
return streamedRest.stream
.transform(utf8.decoder)
.transform(json.decoder)
.expand((data) => (data as List))
.map((data) => Beer.fromJSON(data));
}
I'm not sure how to implement the right way because I'm new to Flutter as well. If you need other code feel free to ask, and any help would be appreciated. Thanks!
CReate AsyncMemoizer in State Class
AsyncMemoizer _memoizer = AsyncMemoizer();
Now Change
Future listenForBeers() async {
return this._memoizer.runOnce(() async {
final Stream<Beer> stream = await getBeers();
stream.listen((Beer beer) => setState(() => _beers.add(beer)));
)};
}
Future refreshBeers() async {
_memoizer = AsyncMemoizer();
return listenForBeers();
}
Details at https://medium.com/saugo360/flutter-my-futurebuilder-keeps-firing-6e774830bc2
Initialize stream in initstate and keep referance like this.
Stream<Beer> stream;
#override
void initState(){
super.initState();
stream = await getBeers();
stream.listen((Beer beer) => setState(() => _beers.add(beer)));
}