Creating bidirectional PageView scrolling in Flutter? - widget

How would you implement bi-directional page scrolling functionality in Flutter? Multiple PageViews in a given Scaffold don't seem to work, nor do nested PageViews. My feeling is that NestedScrollView might offer a solution, but I'm struggling to figure out the implementation.

Multi Directional PageView:
You could potentially create some sort of trickery for this, i.e. set amount of pages to 2000 and then set the initial page as 1000. Therefore creating a PageView with 1000 +/- left right scrolls?
Widget:
#override
Widget build(BuildContext context) {
Future<void>.delayed(Duration.zero, () => _diaryBloc.buildComplete());
return Container(
height: MediaQuery.of(context).size.height,
child: PageView.builder(
controller: _diaryBloc.pageController,
itemBuilder: (BuildContext context, int position) {
return _buildPage(_diaryBloc.getDateFromPosition(position));
},
itemCount: 2000,
),
);
}
BLoC or other controller
final PageController pageController = PageController();
final int initialPage = 1000;
Future<void> buildComplete() async {
pageController.jumpToPage(initialPage);
}
DateTime getDateFromPosition(int position) {
if (position < 0) {
return currentDateTime.subtract(Duration(days: position - initialPage));
} else {
return currentDateTime.add(Duration(days: position - initialPage));
}
}
Might not be the ideal but is extremely simple and seems to work well.

Related

How to make the order of async function executions in Flutter consistent?

In the quiz app I'm making, this controller reads a local JSON item and creates a shuffled list with eight items inside. Then, a session screen is launched with the said list to be used in making the UI.
class SessionController{
List _currentSessionList = [];
SessionController(BuildContext context, String lessonID) {
readJsonForItems(lessonID);
launchSessionScreen(context, lessonID);
int _currentSessionIndex = 0;
}
SessionController.feature();
Future<void> readJsonForItems(String lessonID) async {
final String response = await rootBundle.loadString('assets/model/complete_items.json');
final listFromJson = await json.decode(response);
createSessionList(listFromJson, lessonID);
}
void createSessionList(var importedList, String lessonID) async {
var sessionItems = importedList[lessonID];
sessionItems.shuffle();
_currentSessionList = List.from(sessionItems.take(8));
}
void launchSessionScreen(BuildContext context, String lessonID) async {
Navigator.push( context,
MaterialPageRoute(builder: (context) => SessionScreen(_currentSessionList, lessonID)),
);
}
List getCurrentSessionList() {
return _currentSessionList;
}
int getCurrentSessionIndex() {
return _currentSessionIndex;
}
Here is the code for the SessionScreen class:
class SessionScreen extends StatefulWidget {
List currentSessionList;
String lessonID;
List currentLessonContents;
SessionScreen(this.currentSessionList, this.lessonID, this.currentLessonContents, {super.key});
#override
State<SessionScreen> createState() => _SessionScreenState();
}
class _SessionScreenState extends State<SessionScreen> {
late List sessionItems;
String lessonID = "", question = "", prompt = "", correctAnswer = "";
List<dynamic> choices = [];
#override
void initState(){
super.initState();
setInitialContents();
}
void setInitialContents() async {
sessionItems = List.from(widget.currentSessionList);
updateContents();
}
void updateContents() async {
int index = feature.getCurrentSessionIndex();
setState(() {
question = sessionItems[index]["question"];
prompt = sessionItems[index]["prompt"];
choices = sessionItems[index]["choices"];
correctAnswer = sessionItems[index]["correct_answer"];
audioFileDirectory = sessionItems[index]["audio_file"];
});
}
#override
Widget build(BuildContext buildContext) {
return MaterialApp(
home: Scaffold(
backgroundColor: const Color(0xffF5F1E6),
appBar: sessionAppBar(fromCompletionScreen: false, context: context),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
width: 400,
margin: const EdgeInsets.all(30),
child: Column(
children: <Widget> [
// I removed the other UI components for brevity's sake.
),
]
),
),
],
),
),
)
);
}
These are the issues:
**In the first session, **the program builds the SessionScreen prematurely, without the list of contents like the question, prompt, choices, etc. Because of this, the widgets are loaded, but they are empty.
However, if this session is quitted and a new session starts, the screen already works as expected. So, from the second session onwards, the list of contents are already loaded with the UI and works as expected.
Clearly, there is an issue with the first session. What am I missing here?
I tried loading the JSON file from the launching of the app, but still, getting the JSON contents and loading them on the screen are late in the first session. I'm new to asynchronous programming, so thank you for you patience. :-)
There are several issues with the code presented.
readJsonForItems() is asynchronous, but it is not awaited in the SessionController constructor. Therefore, launchSessionScreen() may be called before readJsonForItems() has completed. Note that constructors cannot be asynchronous. So for SessionController, consider splitting the concepts of construction and initialization. Maybe something like
SessionController() {
int _currentSessionIndex = 0;
}
Future<void> start(BuildContext context, String lessonID) async {
await readJsonForItems(lessonID);
launchSessionScreen(context, lessonID);
}
1B. While you're learning asynchronous programming in Dart, consider adding the following to analysis_options.yaml to help catch issues like the foregoing:
linter:
rules:
unawaited_futures: true
Typically, when the UI depends on an asynchronous result, you will need to use a builder of some sort. The builder helps you create the UI after the asynchronous operation upon which the UI depends has completed. In the case presented, FutureBuilder would be an appropriate choice.
How you do this will depend on what you want to achieve, but one possibility might look like this:
Future<List> readJsonForItems(String lessonID) async {
final String response = await rootBundle.loadString('assets/model/complete_items.json');
final listFromJson = await json.decode(response);
return createSessionList(listFromJson, lessonID);
}
List createSessionList(var importedList, String lessonID) {
var sessionItems = importedList[lessonID];
sessionItems.shuffle();
return List.from(sessionItems.take(8));
}
void launchSessionScreen(BuildContext context, String lessonID) async {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return FutureBuilder(
future: readJsonForItems(lessonID),
builder: (context, snapshot) {
if (snapshot.hasData) {
return SessionScreen(snapshot.data, lessonID);
}
else {
return SizedBox.shrink();
}
}
);
}
),
);
}
In this example, you'd call launchSessionScreen() to start a lesson. It kicks of the asynchronous call (readJsonForItems(lessonID)), and uses a FutureBuilder to show nothing before the future has completed, and a SessionScreen containing the loaded list after the future has completed.
(You could substitute a loading spinner in place of SizedBox.shrink() to offer some feedback to the user.)
createSessionList() and launchSessionScreen() are marked async but should not be. They're not asynchronous.

how to draw some markers and make it different each other (flutter google map)

i have tried this to customize marker on flutter_google_maps and this to change widget into bytes, since we could change marker using bytes, not widget.
i actually solve the problem if i use only one type of marker like this:
but things are different where the requirement design just like this:
so how do i solve the problem?
here some code i use, but the result output is first image above, not as expected.
-> method to change widget into image
import 'dart:ui' as ui;
GlobalKey<ScaffoldState> keyScaffold = new GlobalKey<ScaffoldState>();
Future<Uint8List> _capturePng() async {
try {
RenderRepaintBoundary boundary =
keyScaffold.currentContext.findRenderObject();
ui.Image image = await boundary.toImage(pixelRatio: 3.0);
ByteData byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
return pngBytes;
} catch (e) {
print(e);
}
}
bool rendering = true;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Peta'),
),
body: rendering
? renderWidgetToImage()
: renderGoogleMap()
);
-> method to render widget before converted
String title;
Widget renderWidgetToImage() {
return RepaintBoundary(
key: keyScaffold,
child: Container(
margin: EdgeInsets.only(top: 30, left: 10, right: 10, bottom: 20),
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
color: Colors.green,
),
child: Text(
title,
style: TextStyle(
fontSize: 10
),
)),
);
}
-> method to programmatically add marker using widget
final Set<Marker> _markers = {};
#override
void initState() {
super.initState();
var arrMarker = <MarkerMap>[
MarkerMap("Text Widget 3","123",3.59196,98.672226),
MarkerMap("Text Widget 2","456",3.49196,97.572226),
MarkerMap("Text Widget 1","789",3.39196,97.772226),
];
for(int i =0; i< arrMarker.length; i++) {
setState(() {
this.title = arrMarker[i].title;
});
BitmapDescriptor.fromAssetImage(
ImageConfiguration(size: Size(48, 48)), DefaultImageLocation.iconAnalog)
.then((onValue) async {
var png = await _capturePng(keyScaffold);
setState(() {
this.myIcon = BitmapDescriptor.fromBytes(png);
this.rendering = false;
});
setState(() {
_markers.add(
Marker(
markerId: MarkerId(arrMarker[i].id),
position: LatLng(arrMarker[i].pos1, arrMarker[i].pos2),
icon: BitmapDescriptor.fromBytes(png),
),
);
});
});
setState(() {
this.rendering = true;
});
}
any help would be appreciated, thank you
Currently, this is now possible using the steps provided in this blog.
As mentioned in the intro:
We need to paint a Widget and then convert it to bitmap. But its
tricky because you cant simply do that. we have to place into widget
tree and fetch its painted bitmap.
For a rough summary, the steps mentioned were:
First
We need to get our bitmaps right after they are drawn. There
are multiple ways to do this, I use tiny AfterLayoutMixin available
here.
Second
Lets create our custom widget, yay!
My widget accepts name as an parameter. I used ClipPath for the
triangular Pointer arrow.
Third
Lets create a location object and list of locations and a
method to generate widgets from the location list.

How do I display images from a JSON file in a carousel(slider) in Flutter

I have a local json file assets/properties.json in which key "image" has [5 different images] stored with other keys as well like name, place, etc. I want this images be displayed in a carouselSlider.
I have searched but cant find something specific to what i am trying to do.
import 'package:flutter/material.dart';
import 'package:carousel_pro/carousel_pro.dart';
import 'package:flutter_test_app/test_page.dart';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'package:flutter_test_app/propery_details_widget.dart';
class PropertyDetails extends StatefulWidget {
#override
_PropertyDetailsState createState() => _PropertyDetailsState();
}
class _PropertyDetailsState extends State<PropertyDetails> {
List properties;
Future<String> loadJsonData() async {
var jsonText = await rootBundle.loadString("assets/properties.json");
setState(() {
properties = json.decode(jsonText);
});
return 'success';
}
int index = 1;
List<String> listaTela = new List();
CarouselSlider instance;
#override
void initState() {
super.initState();
this.loadJsonData();
listaTela.add("assets/images/houses/house.jpg");
listaTela.add('assets/images/houses/house1.jpg');
listaTela.add('assets/images/houses/house2.jpg');
listaTela.add('assets/images/houses/house3.jpg');
listaTela.add('assets/images/houses/house4.jpg');
}
#override
Widget build(BuildContext context) {
instance = new CarouselSlider(
autoPlay: true,
autoPlayDuration: new Duration(seconds: 2),
items: listaTela.map((it) {
return new Container(
width: MediaQuery.of(context).size.width,
// margin: new EdgeInsets.symmetric(horizontal: 5.0),
decoration: new BoxDecoration(
// color: Colors.amber,
),
child: new Image.asset(it),
);
}).toList(),
height: 200,
);
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("Test App"),
),
body: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
children: <Widget>[
instance,
Flexible(
fit: FlexFit.tight,
child: Container(
child: Details(),
),
)
],
),
),
);
}
}
Instead of calling images from assests listaTela.add("assets/images/houses/house.jpg"); like this i want to call them from my "image" key in JSON file. outside my Carousel i can call my images by properties[index]["image"][0],
Try this. (There's a chance that the map won't work because it will have the wrong type. I don't think you included the whole json, as you are indexing it by index, yet you don't show a [] surrounding the json indicating a json array.)
class _PropertyDetailsState extends State<PropertyDetails> {
List properties;
int index = 1;
Future<void> loadJsonData() async {
var jsonText = await rootBundle.loadString("assets/properties.json");
setState(() {
properties = json.decode(jsonText);
});
}
#override
void initState() {
super.initState();
loadJsonData();
}
#override
Widget build(BuildContext context) {
Widget carousel = properties == null
? CircularProgressIndicator()
: CarouselSlider(
items: properties[index]["image"].map((it) {
return new Container(
width: MediaQuery.of(context).size.width,
decoration: new BoxDecoration(),
child: new Image.asset(it),
);
}).toList(),
autoPlay: true,
autoPlayDuration: new Duration(seconds: 2),
height: 200,
);
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text("Test App"),
),
body: Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
children: <Widget>[
carousel,
Flexible(
fit: FlexFit.tight,
child: Container(
child: Details(),
),
)
],
),
),
);
}
}
As far as I can understand you problem, there are a couple of challenges.
you probably want to map the JSON to Dart objects
you want to display a set of images in the Slider starting from index
mechanics to update you widget tree when you want to update the slider
JSON and Dart
Takes some getting used to built package:built_value is a nice way to go. It will allow you to have Dart object or list of objects mapped from a JSON file. Or, if you want to go simpler, you could do what's described here: https://flutter.dev/docs/development/data-and-backend/json#serializing-json-manually-using-dartconvert
Images from index & update
Basically, what you want to do in your initState is:
load image data from json -> set start index -> somehow get Flutter to map N image paths to Image widgets and update tree
A pseudo-code like initState could look like so:
void initState() {
super.initState();
// step 1: load json and map it to Dart objects
this.loadFromJson().then(() {
// loadFromJson returns with a Future, `then` allows you to do computation after it returns
setState(() {
// setting property in setState tells Flutter: hey buddy, stuff happened, rebuild!
this.startIndex = 0;
});
});
}
inside your build:
// [...]
CarouselSlider(
items: listaTela.skip(this.startIndex).take(5).map((imgObj) => Image.asset(imgObj.assetPath))
);
Hope I understood your problem and gave you relevant answer.

type 'Future<int>' is not a subtype of type 'int' with Flutter

I have a Stateful Class that build a grid if Items.
This items are retrieved by an HTTP call to an external server.
I am Using PagewiseGridView (https://pub.dartlang.org/packages/flutter_pagewise) to create my grid.
PagewiseGridView has a parameter called totalCount that is an INT, and it works perfectly when I get the totals from a Future because I've set an INT var inside initstate() to change the value after returning the Future.
The real problem is when I do a search on my external server.
For the search I am building a class: ShowSearch extends SearchDelegate and Search delegate has widget called buildResult that will show the results that I get from my external call.
I am also using PagewiseGridView to build my results layout.
Here's the code:
#override
Widget buildResults(BuildContext context) {
// TODO: implement buildResults
return PagewiseGridView(
pageSize: 10,
totalCount: BackendService.getSearchTotals(query),
crossAxisCount: 2,
mainAxisSpacing: 10.0,
crossAxisSpacing: 5.0,
//childAspectRatio: 0.802,
padding: EdgeInsets.all(5.0),
itemBuilder: _itemBuilder,
pageFuture: (pageIndex){
return BackendService.getSearchItems(pageIndex, query);
},
);
}
"totalCount: BackendService.getSearchTotals(query)" returns a Future, so it does not work because it requires an INT (totalCount: 100, works).
How can I solve this?
FULL CLASS (Different Method):
class ShowSearch extends SearchDelegate<BackendService> {
Color _mainColor = const Color(0xFFCA0813);
int _searchTotalCounter;
#override
void initState() {
//super.initState();
getSearchTotalCounter();
}
void getSearchTotalCounter() async {
_searchTotalCounter = await getSearchTotals(query);
//setState(() {});
}
Future<int> getSearchTotals(query) async {
var myRes = await http.get(Uri.encodeFull("https://www.mydomain.io/wp-json/test/v1/stats/search/?s=$query"), headers: {"Accept": "application/json"});
var myResBody = json.decode(myRes.body);
return myResBody["count_total"];
}
#override
List<Widget> buildActions(BuildContext context) {
// TODO: implement buildActions
return [IconButton(
icon: Icon(Icons.clear),
onPressed: (){
query = "";
}
)];
}
#override
Widget buildLeading(BuildContext context) {
// TODO: implement buildLeading
return IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
close(context, null);
});
}
#override
Widget buildSuggestions(BuildContext context) {
// TODO: implement buildSuggestions
return Column(
children: <Widget>[
Padding(
padding: EdgeInsets.all(10.0),
child: Center(
child: Text(query)
)
)
],
);
}
#override
Widget buildResults(BuildContext context) {
// TODO: implement buildResults
return PagewiseGridView(
pageSize: 10,
totalCount: _searchTotalCounter,
//totalCount: 20 //IT WORKS!
crossAxisCount: 2,
mainAxisSpacing: 10.0,
crossAxisSpacing: 5.0,
//childAspectRatio: 0.802,
padding: EdgeInsets.all(5.0),
itemBuilder: _itemBuilder,
pageFuture: (pageIndex){
return BackendService.getSearchItems(pageIndex, query);
},
);
}
}
I've commented super.initState() and setState() because are giving an error (error: The method 'initState' isn't defined in a superclass of 'SearchDelegate'.) and (error: The method 'setState' isn't defined for the class 'ShowSearch'.).
https://www.mydomain.io/wp-json/test/v1/stats/search/?s=$query
returns
{"status":"ok","total":10,"count_total":502,"pages":51,"query":"beans"}
first you can add totalCount: count, and set count to 0 then call this getCount() method mentioned here what will happen , first BackendService.getSearchTotals(query) will be called and when it finishes it will return a future, then you can use the result
to update the count and rebuild using setState ,
ps:you can show a circular loader while the getCount is working.
Future getCount() async {
//here you can call the function and handle the output(return value) as result
BackendService.getSearchTotals(query).then((result) {
// print(result);
setState(() {
//handle your result here and update the count.
//update build here.
});
}).catchError(handleError);//you can call handleError method show an alert or to try again
}

Implementing a bidirectional listview in Flutter

How do you implement a bidirectional scroll view in Flutter? ListView has a scrollDirection field however it can only take either Axis.horizontal or Axis.vertical. Is is possible to have both?
Here's a potential solution using an outer SingleChildScrollView. You could also use a PageView of multiple ListViews if you're ok with the offscreen ListViews being torn down.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new MyHomePage(),
));
}
class MyHomePage extends StatelessWidget {
Widget build(BuildContext context) {
ThemeData themeData = Theme.of(context);
return new Scaffold(
body: new SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: new SizedBox(
width: 1000.0,
child: new ListView.builder(
itemBuilder: (BuildContext context, int i) {
return new Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: new List.generate(5, (int j) {
return new Text("$i,$j", style: themeData.textTheme.display2);
}),
);
},
),
),
),
);
}
}
See my answer on this question that I posted and self-answered. I'm not aware of a way to do it with a single Scrollable, although I can imagine it being useful.
You can't easily solve this with an infinite-length ListView.builder
because it only goes in one direction. If you want to wrap in both
directions, it is possible to simulate bidirectional wrapping with a
Stack of two Viewports going in opposite directions.
There's a code sample on the question too (you might have to modify the answer a bit if you don't want wrapping).