how to get a data from HtmlElementView in flutter-web - html

i am having a trouble with adding webview in side flutter web
import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/key.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'dart:ui' as ui;
import 'dart:html' as html;
class UserAddress3 extends StatefulWidget {
const UserAddress3({Key? key}) : super(key: key);
#override
State<UserAddress3> createState() => _UserAddress3State();
}
class _UserAddress3State extends State<UserAddress3> {
#override
void initState() {
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory(
'html.iframeElement',
(int viewId) => html.IFrameElement()
..src = 'https://daum_postcode_mobile'
..style.width = '100%'
..style.height = '100%'
..style.border = 'none');
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('주소 검색'),
backgroundColor: Colors.white,
),
body: Container(
child: HtmlElementView(
viewType: 'html.iframeElement',
),
),
);
}
}
'https://daum_postcode_mobile' this link leads to where we can find a address
so the web-view displays fine but i can't have a datas if I click the address
I thought it would work as same as how Webview worked in Flutter App but it seems totaly different...
Is there any one knows how to get datas from HtmleElementView???
p.s and this is my codes which used in my app
class KakaoAddress extends StatefulWidget {
#override
_KakaoAddressState createState() => _KakaoAddressState();
}
class _KakaoAddressState extends State<KakaoAddress> {
final Completer<WebViewController> _controller = Completer<WebViewController>();
#override
Widget build(context) {
return Scaffold(
appBar: AppBar(
iconTheme: IconThemeData(
color: Colors.black,
),
titleSpacing: 0.0,
elevation: 0.0,
title: const Text(
'주소 검색',
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: Colors.black),
),
actions: <Widget>[
SampleMenu(),
],
backgroundColor: Colors.white,
),
body: Builder(builder: (context) {
return WebView(
initialUrl: 'https://daum_postcode_mobile',
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
_controller.complete(webViewController);
},
// ignore: prefer_collection_literals
javascriptChannels: <JavascriptChannel>[
_toasterJavascriptChannel(context),
].toSet(),
);
}),
);
}
JavascriptChannel _toasterJavascriptChannel(context) {
return JavascriptChannel(
name: 'Daum',
onMessageReceived: (JavascriptMessage message) async {
final myJsonAsString = message.message;
dynamic received = json.decode(myJsonAsString);
dynamic address = received["address"];
Navigator.pop(context, address);
});
}
}
class SampleMenu extends StatelessWidget {
#override
Widget build(context) {
return FutureBuilder<WebViewController>(
builder: (context, AsyncSnapshot<WebViewController> controller) {
return IconButton(
icon: Icon(
Icons.close,
size: 25.0,
),
onPressed: () {
Navigator.pop(context);
});
},
);
}
}

I made something similar, which may help you. I handled mouse clicks performed inside HtmlElementView, with Dart code, the following way:
In the code passed to ui.platformViewRegistry.registerViewFactory() I used DOM manipulation to attach a listener to the onClick event of an element.
You can do the same with a custom event and trigger that event anytime from Javascript to call and pass data to the Dart side.

Related

flutter: bottomNavigationBar cannot switch between html pages using WebView

Goal
I use bottomNavigationBar to switch between a few pages. Two of the pages use WebView to show local html files.
Problem
I found that when I switch between these pages, the pages with pure flutter widgets can load perfectly. But the HTML pages will only show the initially loaded page on switching.
For example, If I have two navigator buttons leading to Page1 and Page2, then at runtime, if I tapped Page1 button first, then tapped Page2 button, then the WebView would still show Page1 instead of Page2. This is wrong.
Code
Here is the HTML page code I use
class LocalLoader {
Future<String> loadLocal(String filename) async {
return await rootBundle.loadString('assets/doc/$filename');
}
}
class HtmlPage extends StatelessWidget {
final String htmlFile;
HtmlPage({
#required this.htmlFile
});
#override
Widget build(BuildContext context) {
return Container(
child: FutureBuilder<String>(
future: LocalLoader().loadLocal(htmlFile),
builder: (context, snapshot) {
if (snapshot.hasData) {
return WebView(
initialUrl: Uri.dataFromString(snapshot.data,
mimeType: 'text/html',
// CAUTION
// - required for non-ascii chars
encoding: Encoding.getByName("UTF-8")
).toString(),
javascriptMode: JavascriptMode.unrestricted,
);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
} else {
print('undefined behaviour');
}
return CircularProgressIndicator();
},
),);
}
}
Then with my bottomNavigationBar, I handle the tap event:
class MyFlutterView extends StatefulWidget {
#override
_MyFlutterViewState createState() => _MyFlutterViewState();
}
class _MyFlutterViewState extends State<MyFlutterView> {
final Keys keys = Keys();
int _iSelectedDrawerItem = 3; // self
int _iSelectedNavItem = 0;
static List<Widget> _widgetOptions = <Widget>[
MyFlutterPlaceholder(title: 'Index 0: MyFlutter'),
MyPage(htmlFile: 'page1.html'),
MyPage(htmlFile: 'page2.html'),
];
void _onItemTapped(int index) {
setState(() {
_iSelectedNavItem = index;
});
}
#override
Widget build(BuildContext context) {
final deviceSize = MediaQuery.of(context).size;
final appBar = AppBar(
backgroundColor: WidgetColors.menubar,
title: Text('MyFlutter'),
);
return Scaffold(
appBar: appBar,
endDrawer: NavDrawer(
keys: keys,
iSelectedDrawerItem: _iSelectedDrawerItem,
),
body: Container(
decoration: BoxDecoration(
gradient: WidgetColors.canvas,
),
child: _widgetOptions.elementAt(_iSelectedNavItem),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex : _iSelectedNavItem,
type: BottomNavigationBarType.fixed,
backgroundColor: WidgetColors.menubar,
fixedColor: WidgetColors.myColor,
// selectedItemColor: WidgetColors.myColor,
unselectedItemColor: Colors.white,
selectedIconTheme: IconThemeData(color: WidgetColors.myColor),
// unselectedIconTheme: IconThemeData(color: Colors.white),
items: [
BottomNavigationBarItem(
label: 'MyFlutter',
icon: Icon(Icons.build)
),
BottomNavigationBarItem(
label: 'Page1-HTML',
icon: Icon(Icons.help,),
),
BottomNavigationBarItem(
label: 'Page2-HTML',
icon: Icon(Icons.info_outline_rounded),
),
],
onTap: _onItemTapped),
);
}
}
I've also tried StatefulWidgets but the problem persists.
Workaround
The only workaround I have right now is to derive from the HtmlPage class for every single page I have, like this:
class Page1 extends HtmlPage {
Page1() : super(htmlFile: 'page1.html');
}
class Page2 extends HtmlPage {
Page2() : super(htmlFile: 'page2.html');
}
After this, the HTML pages will switch and load as expected.
Question
How should I fix this? Should I work on loading the HTML file more explicitly? I thought setState would handle the loading for me automatically and this certainly applies to the pure flutter widget page (MyFlutterPlaceholder class in the code above).
Also, I ensured that the url loading was called every time I switch page through the nav bar.
You can copy paste run full code below
I simulate this case with the full code below
Step 1: Use AutomaticKeepAliveClientMixin
class _WebViewKeepAlive extends State<WebViewKeepAlive>
with AutomaticKeepAliveClientMixin {
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
Step 2: Do not direct put function in future attribute, future: LocalLoader().loadLocal(htmlFile), Please use below way
Future<String> _future;
#override
void initState() {
_future = _getUrl(widget.url);
super.initState();
}
return FutureBuilder(
future: _future,
Step 3: I use PageView in this case
working demo
full code
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyPortalPage(title: 'Flutter Demo Home Page'),
);
}
}
class MyPortalPage extends StatefulWidget {
MyPortalPage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyPortalPageState createState() => _MyPortalPageState();
}
class _MyPortalPageState extends State<MyPortalPage> {
int _currentIndex = 0;
PageController _pageController = PageController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SizedBox.expand(
child: PageView(
controller: _pageController,
children: <Widget>[
Page1(),
WebViewKeepAlive(url: "https://flutter.dev/"),
WebViewKeepAlive(url: "https://stackoverflow.com/"),
Center(child: Text("Settings")),
],
onPageChanged: (int index) {
print("onPageChanged");
setState(() {
_currentIndex = index;
});
},
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
selectedItemColor: Colors.amber[800],
unselectedItemColor: Colors.blue,
onTap: (index) {
print("onItemSelected");
setState(() => _currentIndex = index);
_pageController.jumpToPage(index);
},
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.apps),
label: 'Challenges',
),
BottomNavigationBarItem(
icon: Icon(Icons.people),
label: 'Users',
),
BottomNavigationBarItem(
icon: Icon(Icons.message),
label: 'Messages',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
),
],
),
);
}
}
class Page1 extends StatefulWidget {
const Page1({Key key}) : super(key: key);
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> with AutomaticKeepAliveClientMixin {
#override
Widget build(BuildContext context) {
super.build(context);
return ListView.builder(itemBuilder: (context, index) {
return ListTile(
title: Text('Lorem Ipsum'),
subtitle: Text('$index'),
);
});
}
#override
bool get wantKeepAlive => true;
}
class WebViewKeepAlive extends StatefulWidget {
final String url;
WebViewKeepAlive({Key key, this.url}) : super(key: key);
#override
_WebViewKeepAlive createState() => _WebViewKeepAlive();
}
class _WebViewKeepAlive extends State<WebViewKeepAlive>
with AutomaticKeepAliveClientMixin {
Future<String> _future;
#override
bool get wantKeepAlive => true;
Future<String> _getUrl(String url) async {
await Future.delayed(Duration(seconds: 1), () {});
return Future.value(url);
}
#override
void initState() {
_future = _getUrl(widget.url);
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
future: _future,
builder: (context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('none');
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.active:
return Text('');
case ConnectionState.done:
if (snapshot.hasError) {
return Text(
'${snapshot.error}',
style: TextStyle(color: Colors.red),
);
} else {
return WebView(
initialUrl: snapshot.data,
javascriptMode: JavascriptMode.unrestricted,
);
}
}
});
}
}

Flutter. Add a json.decode value to a String that can be used in the UI

Maybe this is a simple question, but I can't find the answer to it.
My app has 2 screens. 1st has a single button
onPressed: () {
fetchCurrentTitle();
Navigator.push(context,
MaterialPageRoute(builder: (context) => Screen2Widget()));
},
fetchCurrentTitle() method fetches data from json and decodes it.
I can see the return using:
final streamFullTitle = json.decode(response.body)['data'][0]['title'];
print(streamFullTitle);
I get the desired response of the current title in the console.
In the 2nd screen I have a hardcoded List. Where items have these values:
class List {
String id;
String streamer;
String logoUrl;
String title;
}
The first three attribute in List class dont need to change so they are hardcoded. I just need to assign the title value fromfetchCurrentTitle() to the String title. in class List.
Look of one of my list items
My fetchCurrentTitle() works as intended
Future<String> fetchCurrentTitle() async {
http.Response response = await http.get(...
I want the user to push the button on the first screen to go to the second screen and show a spinner with title "looking for title" and then get the new title instead of waiting fetchCurrentTitle() to complete only entering the second screen.
Thank you in advance.
You can try to run your fetchCurrentTitle() in the second screen on
void initState() {
super.initState();
fetchCurrentTitle()
/// show or set visibility for loading spinner
}
After you get the title value, you can simply assign new title by
List existingList = new List(id,streamer,logoUrl,title);
existingList.id = titleValueFromApi
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:sample_project_for_api/Employee.dart';
void main() => runApp(MaterialApp(
title: "App",
home: MyApp(),
));
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('home page'),
),
body: Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: RaisedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => SecondScreen()));
},
child: const Text('First Page')),
)),
);
}
}
class SecondScreen extends StatefulWidget {
#override
_SecondScreenState createState() => _SecondScreenState();
}
class _SecondScreenState extends State<SecondScreen> {
bool _isLoading = false;
Employee sampleData;
#override
void initState() {
super.initState();
getYouData();
}
Future<String> loadPersonFromAssets() async {
return await rootBundle.loadString('json/parse.json');
}
getYouData() async {
setState(() {
_isLoading = true;
});
String jsonString = await loadPersonFromAssets();
final data = employeeFromJson(jsonString);
sampleData = data;
// this is where you get the data from the network
Future.delayed(const Duration(seconds: 5), () {
// this is sample delay for you to know the delay.
// you can say this is loading your data
setState(() {
_isLoading = false;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: Container(
child: _isLoading
//here give your message getting the title
? CircularProgressIndicator()
: Card(
child: Column(
children: <Widget>[
Text('your 1st hardcoded text'),
Text('your 2st hardcoded text'),
Text('your 3st hardcoded text'),
Text(
'This is your dynamic data after fetching :${sampleData.employeeName}')
],
),
),
),
),
),
);
}
}
check out this example you will give you an idea of what to do
1) going from one page to another page
2) loading you data , while that showing the spinner
3) on fetching the data update the UI.
let me know about this.
Thanks.
Try this,
import 'dart:convert';
import "package:flutter/material.dart";
import 'package:http/http.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
home: Page1(),
);
}
}
class Page1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Page 1")),
body: Center(
child: RaisedButton(
child: Text("Goto Page2"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Page2(),
),
);
},
),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2> {
Future<void> _initLoader;
String _title;
#override
void initState() {
_initLoader = _loadInitData();
super.initState();
}
Future<void> _loadInitData() async {
await Future.delayed(Duration(seconds: 2));
//_title = await fetchCurrentTitle();
_title = "This is the Loaded Title";
}
Future<String> fetchCurrentTitle() async {
Response response = await get("...");
return json.decode(response.body)['data'][0]['title'];
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Page 2")),
body: FutureBuilder(
future: _initLoader,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CircularProgressIndicator(),
const SizedBox(height: 8.0),
Text("Looking for Title"),
],
),
);
else if (snapshot.hasError)
return Center(
child: Text("Error: ${snapshot.error}"),
);
else
return Center(
child: Text("$_title"),
);
},
),
);
}
}

Flutter: Loading json data within second page Flutter Class timing out without error

I've generated a listView from the Pokemon API of a list of Pokemon, I then have onTap events to trigger a new page/class, where I'm passing the name and URL from the API to the new secondPage Class/Screen.
I need to make a second request in this new page because the API Url needs to change to grab specific details but my request seems to be timing out..
Here is my code: If loaded into a new project the first screen should function fine, loading a bunch of Pokemon and their API specific URLs into a listView.
I can successfully pass the name and URL onto the second screen because they do appear in the Appbar.
However when loading the new json data it seems to be timing out without any error.
Does anyone have any advice for a newbie trying to get his footing?
import 'dart:async';
import 'dart:convert';
import 'package:basic_utils/basic_utils.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
TextEditingController editingController = TextEditingController();
String url = 'https://pokeapi.co/api/v2/pokemon/?limit=151';
List data;
Future<String> makeRequest() async {
var response = await http
.get(Uri.encodeFull(url), headers: {"Accept": "application/json"});
setState(() {
var extractData = json.decode(response.body);
data = extractData["results"];
});
}
#override
void initState() {
this.makeRequest();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Pokemon List'),
),
body: Container(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(15.0),
child: new TextField(
onChanged: (value) {
},
decoration: InputDecoration(
labelText: "Search",
hintText: "Search",
contentPadding: const EdgeInsets.all(10.0),
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(6.0))
),
),
),
),
Expanded(
child: new ListView.builder(
itemCount: data == null ? 0 : data.length,
itemBuilder: (BuildContext context, i) {
return new ListTile(
title: new Text(StringUtils.capitalize(data[i]["name"])),
subtitle: new Text(data[i]["url"]),
// leading: new CircleAvatar(
// backgroundImage:
// new NetworkImage(data[i]["picture"]["thumbnail"]),
// ),
onTap: () {
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) =>
new SecondPage(data[i])
)
);
},
);
}
),
),
],
),
),
);
}
}
// Class for getting Specific Details on SecondPage
class Post {
final String name;
final int weight;
Post({this.name, this.weight});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
name: json['name'],
weight: json['weight'],
);
}
}
// New Request for Specific Details
class SecondPage extends StatelessWidget {
SecondPage(this.data);
final data;
Future<Post> fetchPost() async {
final response =
await http.get('https://pokeapi.co/api/v2/pokemon/' + data["name"]);
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON.
return Post.fromJson(json.decode(response.body));
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
Future<Post> post;
#override
void initState() {
fetchPost();
post = fetchPost();
}
#override
Widget build(BuildContext context) =>
new Scaffold(
appBar: new AppBar(
title: new Text(data["name"] + ' - ' + data["url"])),
body: new Center(
child: Column(
children: <Widget>[
Container(
padding: const EdgeInsets.all(30.0),
child: FutureBuilder<Post>(
future: post,
builder: (context, snapshot) {
if (snapshot.hasData) {
Text(snapshot.data.name);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
),
),
],
),
)
);
}
Man, I rewrite your code as I like
import 'dart:async';
import 'dart:convert';
//import 'package:basic_utils/basic_utils.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
TextEditingController editingController = TextEditingController();
String url = 'https://pokeapi.co/api/v2/pokemon/?limit=151';
List data;
Future<String> makeRequest() async {
var response = await http.get(Uri.encodeFull(url), headers: {"Accept": "application/json"});
setState(() {
var extractData = json.decode(response.body);
data = extractData["results"];
});
}
#override
void initState() {
super.initState();
makeRequest();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pokemon List'),
),
body: Container(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(15.0),
child: TextField(
onChanged: (value) {},
decoration: InputDecoration(
labelText: "Search",
hintText: "Search",
contentPadding: const EdgeInsets.all(10.0),
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(6.0))),
),
),
),
Expanded(
child: ListView.builder(
itemCount: data == null ? 0 : data.length,
itemBuilder: (BuildContext context, i) {
return ListTile(
title: Text(data[i]["name"].toString().toUpperCase()),
subtitle: Text(data[i]["url"]),
// leading: CircleAvatar(
// backgroundImage:
// NetworkImage(data[i]["picture"]["thumbnail"]),
// ),
onTap: () {
Navigator.push(
context, MaterialPageRoute(builder: (BuildContext context) => SecondPage(data[i])));
},
);
}),
),
],
),
),
);
}
}
class SecondPage extends StatefulWidget {
Map data;
SecondPage(this.data);
_SecondState createState() => _SecondState();
}
class _SecondState extends State<SecondPage> {
#override
void initState() {
super.initState();
_fetchPost();
}
Map post;
bool isLoad = true;
_fetchPost() async {
setState(() {
isLoad = true;
});
var url = widget.data["url"];
debugPrint(url);
final response = await http.get(url);
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON.
post = json.decode(response.body.toString());
setState(() {
isLoad = false;
});
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load post');
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.data["name"] + ' - ' + widget.data["url"])),
body: _buildPokemon(context),
);
}
Widget _buildPokemon(BuildContext context) {
if (isLoad) return Center(child: CircularProgressIndicator());
return Container(
padding: EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(post['name']),
Text(post['weight'].toString()),
Text(post['height'].toString()),
Image.network(post['sprites']['front_default'])
],
),
);
}
}

Update AppBar title from body widget in Scaffold

I am starting to use flutter and doing the 'Startup Name Generator' tutorial, also modifying the code to do some different things, and have come across an issue I can't solve. I want the AppBar title to show the size of the _suggestions array, but can't get it to work, have tried many different things but to no avail, always getting the exception of trying to use 'setState()' while already in a build.
This is what I currently have:
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Startup Name Generator',
home: RandomWords(),
);
}
}
class RandomWordsState extends State<RandomWords> {
static final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
ValueNotifier<String> appBarTitleNotifier = ValueNotifier('Startup Name Generator: 0');
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(title: ValueListenableBuilder<String>(
valueListenable: appBarTitleNotifier,
builder: (context,value,child) {
return Text(value);
}
)),
body: _buildSuggestions(),
);
}
Widget _buildSuggestions() {
var blder = ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: /*1*/ (context, i) {
if (i.isOdd) return Divider(); /*2*/
final index = i ~/2; /*3*/
if (index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10)); /*4*/
/**** THIS NEXT LINE CAUSES THE ISSUE ****/
appBarTitleNotifier.value = 'Startup Name Generator: ' + _suggestions.length.toString();
}
return _buildRow(_suggestions[index],index);
},
);
return blder;
}
Widget _buildRow(WordPair pair, int i) {
return ListTile(
title: Text(
pair.asPascalCase + ': ' + i.toString(),
style: _biggerFont,
),
);
}
}
class RandomWords extends StatefulWidget {
#override
RandomWordsState createState() => RandomWordsState();
}
I have used a ValueNotifier and listener after doing some research into events, but same exception as when I manually used a setState() call within the ListView itemBuilder (where the issue seems to lie; calling/updating the AppBar title from within the ListView itemBuilder).
I haven't found a solution here so I am asking. Sorry if this has already been answered, but I could not find the solution.
How about using GestureDetector.
onVerticalDragEnd event not executed. but onVerticalDragDown works.
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome to Flutter',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Welcome to Flutter'),
toolbarHeight: 100.0,
),
body: const Center(
child: RandomWords(),
),
),
);
}
}
class RandomWords extends StatefulWidget {
const RandomWords({Key? key}) : super(key: key);
#override
State<RandomWords> createState() => _RandomWordsState();
}
class _RandomWordsState extends State<RandomWords> {
final _suggestions = <WordPair>[];
final _biggerFont = const TextStyle(fontSize: 18.0);
String title = "Startup Name Generator";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: GestureDetector(
onTap: () {
print('tap');
},
onVerticalDragDown : (details) {
print('onVerticalDragDown');
setState(() {
title = "Startup Name Generator ("+_suggestions.length.toString()+")";
});
},
onVerticalDragEnd : (details) {
print('onVerticalDragEnd ');
setState(() {
title = "Startup Name Generator ("+_suggestions.length.toString()+")";
});
},
child:
// Padding(
// padding: const EdgeInsets.all(8.0),
// child: Icon(
// Icons.lightbulb_outline,
// color: Colors.yellow.shade600,
// size: 60,
// ),
// ),
ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (context, i) {
if(i.isOdd) return const Divider();
final index = i ~/ 2;
if(index >= _suggestions.length) {
_suggestions.addAll(generateWordPairs().take(10));
}
return ListTile(
title: Text(
// index.toString() + ":" + _suggestions[index].asPascalCase,
_suggestions[index].asPascalCase,
style: _biggerFont,
),
);
},
),
)
);
}
}

How to create a dynamic TabBarView/ Render a new Tab with a function in Flutter?

So I have been learning flutter in a while and I am stuck in this. Sorry if it is a noobish question. I am currently trying to build something like a Card Tab. The information and widget will be stored in a card.
Imagine something like Tinder, where they have multiple card stack and swipe left and right to navigate.
I plan to create that but I cannot seems to find a way to add/render a new card with a button.
It's like adding something to the list, Flutter will use a ListView builder where we add to the list. But there is no TabBarView builder. Is this something that is not possible to do? I try putting a list inside a tab but it's still wont be the same.
I created some basic skeleton here to help convey my meaning. So the card will be swipe left and right and there is a button in the appBar to add card. Lenght is 2 now and I wanted the button to render the 3rd card. Is this possible?
Thanks in advance!
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new CardStack(),
));
}
class CardStack extends StatefulWidget {
#override
_MainState createState() => new _MainState();
}
class _MainState extends State<CardStack> with SingleTickerProviderStateMixin {
TabController _cardController;
#override
void initState() {
super.initState();
_cardController = new TabController(vsync: this, length: 2);
}
#override
void dispose() {
_cardController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: Colors.grey[300],
appBar: new AppBar(
actions: <Widget>[
new IconButton(
icon: const Icon(Icons.add),
tooltip: 'Add Tabs',
onPressed: null,
),
],
title: new Text("Title Here"),
bottom: new PreferredSize(
preferredSize: const Size.fromHeight(20.0),
child: new Theme(
data: Theme.of(context).copyWith(accentColor: Colors.grey),
child: new Container(
height: 50.0,
alignment: Alignment.center,
child: new TabPageSelector(controller: _cardController),
),
)
)
),
body: new TabBarView(
controller: _cardController,
children: <Widget>[
new Center(
child: new Card(
child: new Container(
height: 450.0,
width: 300.0,
child: new IconButton(
icon: new Icon(Icons.favorite, size: 100.0),
tooltip: 'Favorited',
onPressed: null,
)
),
),
),
new Center(
child: new Card(
child: new Container(
height: 450.0,
width: 300.0,
child: new IconButton(
icon: new Icon(Icons.local_pizza, size: 50.0,),
tooltip: 'Pizza',
onPressed: null,
)
),
),
),
],
),
);
}
}
Problems arise if you need to modify the arrays. They consist in the fact that when modifying an array you do not have the opportunity to use the same controller.
You can use the next custom widget for this case:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
MyHomePageState createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
List<String> data = ['Page 0', 'Page 1', 'Page 2'];
int initPosition = 1;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomTabView(
initPosition: initPosition,
itemCount: data.length,
tabBuilder: (context, index) => Tab(text: data[index]),
pageBuilder: (context, index) => Center(child: Text(data[index])),
onPositionChange: (index) {
print('current position: $index');
initPosition = index;
},
onScroll: (position) => print('$position'),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
data.add('Page ${data.length}');
});
},
child: const Icon(Icons.add),
),
);
}
}
class CustomTabView extends StatefulWidget {
const CustomTabView({
super.key,
required this.itemCount,
required this.tabBuilder,
required this.pageBuilder,
this.stub,
this.onPositionChange,
this.onScroll,
this.initPosition,
});
final int itemCount;
final IndexedWidgetBuilder tabBuilder;
final IndexedWidgetBuilder pageBuilder;
final Widget? stub;
final ValueChanged<int>? onPositionChange;
final ValueChanged<double>? onScroll;
final int? initPosition;
#override
CustomTabsState createState() => CustomTabsState();
}
class CustomTabsState extends State<CustomTabView>
with TickerProviderStateMixin {
late TabController controller;
late int _currentCount;
late int _currentPosition;
#override
void initState() {
_currentPosition = widget.initPosition ?? 0;
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation!.addListener(onScroll);
_currentCount = widget.itemCount;
super.initState();
}
#override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation!.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition!;
}
if (_currentPosition > widget.itemCount - 1) {
_currentPosition = widget.itemCount - 1;
_currentPosition = _currentPosition < 0 ? 0 : _currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && widget.onPositionChange != null) {
widget.onPositionChange!(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount;
setState(() {
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation!.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition!);
}
super.didUpdateWidget(oldWidget);
}
#override
void dispose() {
controller.animation!.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
if (widget.itemCount < 1) return widget.stub ?? Container();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
alignment: Alignment.center,
child: TabBar(
isScrollable: true,
controller: controller,
labelColor: Theme.of(context).primaryColor,
unselectedLabelColor: Theme.of(context).hintColor,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
),
tabs: List.generate(
widget.itemCount,
(index) => widget.tabBuilder(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount,
(index) => widget.pageBuilder(context, index),
),
),
),
],
);
}
void onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange!(_currentPosition);
}
}
}
void onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll!(controller.animation!.value);
}
}
}
Try this.
To make dynamic tab you can use a List and keep appending the list on every button click.
Trick: Clear List and redraw an empty widget and again draw the widgets as per your list.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new CardStack(),
));
}
class DynamicTabContent {
IconData icon;
String tooTip;
DynamicTabContent.name(this.icon, this.tooTip);
}
class CardStack extends StatefulWidget {
#override
_MainState createState() => new _MainState();
}
class _MainState extends State<CardStack> with TickerProviderStateMixin {
List<DynamicTabContent> myList = new List();
TabController _cardController;
TabPageSelector _tabPageSelector;
#override
void initState() {
super.initState();
myList.add(new DynamicTabContent.name(Icons.favorite, "Favorited"));
myList.add(new DynamicTabContent.name(Icons.local_pizza, "local pizza"));
_cardController = new TabController(vsync: this, length: myList.length);
_tabPageSelector = new TabPageSelector(controller: _cardController);
}
#override
void dispose() {
_cardController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: Colors.grey[300],
appBar: new AppBar(
actions: <Widget>[
new Padding(
padding: const EdgeInsets.all(1.0),
child: new IconButton(
icon: const Icon(
Icons.add,
size: 30.0,
color: Colors.white,
),
tooltip: 'Add Tabs',
onPressed: () {
List<DynamicTabContent> tempList = new List();
myList.forEach((dynamicContent) {
tempList.add(dynamicContent);
});
setState(() {
myList.clear();
});
if (tempList.length % 2 == 0) {
myList.add(new DynamicTabContent.name(Icons.shopping_cart, "shopping cart"));
} else {
myList.add(new DynamicTabContent.name(Icons.camera, "camera"));
}
tempList.forEach((dynamicContent) {
myList.add(dynamicContent);
});
setState(() {
_cardController = new TabController(vsync: this, length: myList.length);
_tabPageSelector = new TabPageSelector(controller: _cardController);
});
},
),
),
],
title: new Text("Title Here"),
bottom: new PreferredSize(
preferredSize: const Size.fromHeight(10.0),
child: new Theme(
data: Theme.of(context).copyWith(accentColor: Colors.grey),
child: myList.isEmpty
? new Container(
height: 30.0,
)
: new Container(
height: 30.0,
alignment: Alignment.center,
child: _tabPageSelector,
),
))),
body: new TabBarView(
controller: _cardController,
children: myList.isEmpty
? <Widget>[]
: myList.map((dynamicContent) {
return new Card(
child: new Container(
height: 450.0,
width: 300.0,
child: new IconButton(
icon: new Icon(dynamicContent.icon, size: 100.0),
tooltip: dynamicContent.tooTip,
onPressed: null,
)),
);
}).toList(),
),
);
}
}
Hope this helps :)