flutter - View a web page inside a container - flutter

I'm trying to create a master-detail type container starting with a column of ListTiles on the left side of the screen. When a user taps on an item, a preset URL will then be displayed on the rest of the screen. Tapping a different item displays a different preset URL.
I've looked at the Flutter WebView Plugin and and webview_flutter packages, but either I don't understand them well enough (quite possible!) or they can't yet do everything I want them to to do.
Beside what I just mentioned, if possible I'd also like the web pages to open zoomed to fit the space they're in, but still be pinchable to other sizes.
p.s. I'm new to Flutter and am also confused about widget construction and memory management. If I try using something like a WebView widget, I don't know whether I just code a WebView widget every time I want to open a page, or if I somehow create a single WebView widget, add a controller, and code .loadFromUrl() methods.

You can create a Row with two children. First children will be ListView that will be consisted of ListTiles. Second children will be the WebView. When a user taps on the list tile, load the url with the controller. There is no need to rebuild the WebView every time in your case
Example by using webview_flutter:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
WebViewController _controller;
List pages = ["https://google.com", "https://apple.com"];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
Container(
width: 300,
child: ListView.builder(
itemCount: pages.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(pages[index]),
onTap: () {
if (_controller != null) {
_controller.loadUrl(pages[index]);
}
},
);
},
),
),
Expanded(
child: WebView(
onWebViewCreated: (WebViewController c) {
_controller = c;
},
initialUrl: 'https://stackoverflow.com',
),
),
],
));
}
}

Just wrap the webview inside a SizedBox
SizedBox(
height: 300,
child: WebView()
)

Related

SingleChildScrollView + Controller - Page resets to top when resizing window

I am creating a flutter Web application, but have issues when resizing the window with a SingleChildScrollView + ScrollController.
When I resize the browser window, the page "snaps" back up to the very top. Being a web app, most of the page "sections" are made from Columns with responsively coded widgets as children, with such widgets as Flexible or Expanded. From what I have read, the SingleChildScrollView widget doesn't work well with Flexible or Expanded widgets, so I thought that may be my issue.
For testing purposes, I created a new page with a single SizedBox that had a height of 3000, which would allow me to scroll. After scrolling to the bottom and resizing the window, I was still snapped up to the top of the page. Thus, with or without using Expanded or Flexible widgets, I have the same result.
Test with a SizedBox only
#override
Widget build(BuildContext context) {
return Scaffold(
color: Colors.white,
body: SingleChildScrollView(
controller: controller.scrollController,
primary: false,
child: Column(
children: [
SizedBox(
width: 150,
height: 3000,
),
],
),
),
);
}
I am using Getx with this project to try getting a demo app up and running a bit quicker while I am still learning the core concepts. Below is my controller.
Controller
class HomePageScrollControllerX extends GetxController {
late ScrollController scrollController;
#override
void onInit() {
super.onInit();
scrollController = ScrollController(
initialScrollOffset: 0.0,
keepScrollOffset: true,
);
}
}
Thank you in advance for any insight on this subject!
EDIT
I have added a listener on my ScrollController, which is able to print to the console that I am scrolling. However, the listener does not get called when I resize the window (tested in both Chrome and Edge).
Currently, I believe my only option is to use the listener to update an "offset" variable in the controller, and pass the window's width over to the controller when the widget rebuilds. If done properly, I should be able to use the controller to scroll to the saved offset. Something like this:
if (scrollController.hasClients) {
if (offset > scrollController.position.maxScrollExtent) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
} else if (offset < scrollController.position.minScrollExtent) {
scrollController.jumpTo(scrollController.position.minScrollExtent);
} else {
scrollController.jumpTo(offset);
}
}
However, I feel like this shouldn't be necessary - and I bet this solution would be visually evident to the user.
Edit 2
While I did get this to work with adding the below code just before the return statement, it appears that my initial thoughts were correct. When I grab the edge of the window and move it, it pops up to the top of the window, then will jump to the correct scroll position. It looks absolutely terrible!
#override
Widget build(BuildContext context) {
Future.delayed(Duration.zero, () {
controller.setWindowWithAndScroll(MediaQuery.of(context).size.width);
});
return PreferredScaffold(
color: Colors.white,
body: SingleChildScrollView(
controller: controller.scrollController,
......
I implemented your code without getX by initializing the scrollController as a final variable outside your controller class. I ran it on microsoft edge and did not face the issue you are describing. What's causing your problem is most probably the way you are handling your state management with getX. I'm guessing your onInit function is run multiple times when you are resizing your web page and that's why the page snaps back up. I would recommend logging how many times the onInit function is called.
I found the answer, and it was my scaffold causing the issue - specifically, the scaffold key. But, before that, the Getx usage to get the answer is very easy, so for those of you looking for that particular answer, it is shown below.
Getx Controller
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
class HomePageScrollControllerX extends GetxController {
late ScrollController scrollController;
#override
void onInit() {
super.onInit();
scrollController =
ScrollController(keepScrollOffset: true, initialScrollOffset: 0.0);
}
#override
void onClose() {
super.onClose();
scrollController.dispose();
}
}
Stateless Widget Build Function
class HomePage extends StatelessWidget {
HomePage({
Key? key,
}) : super(key: key);
// All child widgets can use Get.find(); to get instance
final HomePageScrollControllerX controller =
Get.put(HomePageScrollControllerX());
#override
Widget build(BuildContext context) {
return PreferredScaffold(
color: Colors.white,
body: SingleChildScrollView(
controller: controller.scrollController,
primary: false,
... Etc
So, why didn't this work for me? I created a class called "PreferredScaffold" to save myself a few lines of repetitive code.
PreferredScaffold
class PreferredScaffold extends StatelessWidget {
final Widget? body;
final Color? color;
const PreferredScaffold({Key? key, this.body, this.color = Colors.white})
: super(key: key);
#override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> scaffoldState = GlobalKey();
return Scaffold(
key: scaffoldState,
backgroundColor: color,
appBar: myNavBar(context, scaffoldState),
drawer: const Drawer(
child: DrawerWidget(),
),
body: body,
);
}
}
The problem with the above is, when the window is adjusted, the build function is called. When the build function is called for the scaffold, the scaffoldKey is being set. When set, it returns the scroll position back to 0, or the top of the screen.
In the end, I had to make another Controller that would basically hand over the same instance of a key to the scaffold, so it wouldn't be reset when the build function was called.
ScaffoldController
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ScaffoldControllerX extends GetxController {
static ScaffoldControllerX instance = Get.find();
final GlobalKey<ScaffoldState> scaffoldState = GlobalKey();
}
This changed my PreferredScaffold to the following
PreferredScaffold (version 2)
import 'package:flutter/material.dart';
import 'drawer/drawer_widget.dart';
import 'nav/my_navigation_bar.dart';
class PreferredScaffold extends StatelessWidget {
final Widget? body;
final Color? color;
PreferredScaffold({Key? key, this.body, this.color = Colors.white})
: super(key: key);
final ScaffoldControllerX scaffoldControllerX = ScaffoldControllerX();
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldControllerX.scaffoldState,
backgroundColor: color,
appBar: NavBar(context, scaffoldControllerX.scaffoldState),
drawer: const Drawer(
child: DrawerWidget(),
),
body: body,
);
}
}
I hope this helps if someone has a similar situation.

How I can update a listview element without set state?

I have a List view. Builder with an image. The image has an opacity depending on a provider element (provider element is true, the opacity is 1, if it false opacity is 0). Then I have another class, from that class I can update the provider of the list view image, however in order to change the effect of the change, I have to set state the list view widget.
Even if it is good and is working properly, I don't want to do that because when I set state the list view widget, the list view position restart and I need to scroll again at the element of the list view, I know that I can save the scroll position (with a controller) but because my list view is too large, it needs a lot of time to get at the position, so I don't like it.
Any idea ?
If you want to rebuild the UI without calling setState, you can use the help of ValueNotifier and the ValueListenableBuilder widget.
Here is a simplified counter app, if you take a look at the console, you'll see that print('build'); is only called initially running the build method and not called again when calling _increment():
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: PageLoadApp()));
final counter = ValueNotifier<int>(0);
class PageLoadApp extends StatefulWidget {
const PageLoadApp({Key? key}) : super(key: key);
#override
State<PageLoadApp> createState() => _PageLoadAppState();
}
class _PageLoadAppState extends State<PageLoadApp> {
void _increment() {
counter.value++;
}
#override
Widget build(BuildContext context) {
print('build');
return Scaffold(
body: Scaffold(
body: ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, _) {
return Center(
child: Text('$value', style: const TextStyle(fontSize: 30)),
);
}),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
)));
}
}
Additionally, this widget can be a StatelessWidget.
Since you haven't provided a code example, you'll have to adapt this provided example for your case.
Here is a YouTube video by the Google team explaining ValueListenableBuilder

How to transfer the text entered in the input to another screen?

The application first displays the With text: Entered text screen. And there is a button when clicked on which the user gets to another screen where he needs to enter text. It is necessary for me that when the user has entered the text, when returning back to the first screen, this text is displayed. How can this be done?
My code:
import 'package:flutter/material.dart';
class TextScreen extends StatefulWidget {
const TextScreen({Key? key}) : super(key: key);
#override
State<TextScreen> createState() => _TextScreenState();
}
class _TextScreenState extends State<TextScreen> {
final textController = TextEditingController();
#override
void dispose() {
textController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Enter data'),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: textController,
decoration: InputDecoration(labelText: 'Message'),
),
const SizedBox(
height: 20,
),
],
)),
);
}
}
You can add a result when navigating back
Navigator.of(context).pop("test");
And the you can use the result in the previos screen
Navigator.push(
context,
MaterialPageRoute(builder: (context) => TextScreen()),
).then((result) => { ... });
There are several ways to do this. The main question is if you have a specific submit action, such as a button, to confirm using the new text or if the newly entered text should always be used, also when the user uses the back button or back swipe gesture to go back to the previous screen.
Only through submit action
If you want to consider the back button as a 'cancel' operation, and only want to update the value with a specific button, you can return the new value with the Navigator: Navigator.of(context).pop(updatedValue). Where the page was pushed, you can await this result: final updatedValue = await Navigator.of(context).push(editPageRoute);
If you always want the value to update
In this case, you'll need to update this new text in a component that lives above the Navigator which is likely provided by your MaterialApp (or other WidgetApp).
To do so, you can wrap the Navigator in another widget by using the builder function of the MaterialApp. This widget can be obtained through the widget tree and is available in both pages. You can use InheritedWidget or provider to obtain this widget.
You could also keep a component outside of your widget tree that holds this text. Using get_it might be a solution for that, but riverpod would probably allow you to do so as well.

Flutter change a "shared" widget as the route changes, like the navBar of facebook.com

I don't know if I used correct terms in the title. I meant share by being displayed in diffrent pages with the same state, so that even if I push a new page, the “shared” widget will stay the same.
I'm trying to share the same widget across several pages, like the navigation bar of facebook.com.
As I know, Navigator widget allows to build up a seperate route. I've attempted to use the widget here, and it works quite well.
...
Scaffold(
body: Stack(
children: [
Navigator(
key: navigatorKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) => MainPage());
},
// observers: <RouteObserver<ModalRoute<void>>>[ routeObserver ],
),
Positioned(
bottom: 0,
child: BottomBarWithRecord(),
)
],
));
...
To summarize the situation, there used to be only one root Navigator (I guess it's provided in MaterialApp, but anyway), and I added another Navigator in the route under a Stack (which always display BottomBarWithRecord).
This code works perfect as I expected, that BottomBarWithRecord stays the same even if I open a new page in that new Navigator. I can also open a new page without BottomBarWithRecord by pushing the page in the root Navigator: Navigator.of(context, rootNavigator: true).push(smthsmth)
However, I couldn't find a way to change BottomBarWithRecord() as the route changes, like the appbar of facebook.com.
What I've tried
Subscribe to route using navigator key
As I know, to define a navigator key, I have to write final navigatorKey = GlobalObjectKey<NavigatorState>(context);. This doesn't seem to have addListener thing, so I couldn't find a solution here
Subscribe to route using navigator observer
It was quite complicated. Normally, a super complicated solutions works quite well, but it didn't. By putting with RouteAware after class ClassName, I could use some functions like void didPush() {} didPop() didPushNext to subscribe to the route. However, it was not actually "subscribing" to the route change; it was just checking if user opened this page / opened a new page from this page / ... , which would be complicated to deal with in my situation.
React.js?
When I learned a bit of js with React, I remember that this was done quite easily; I just had to put something like
...
const [appBarIndex, setAppBarIndex] = useState(0);
//0 --> highlight Home icon, 1 --> highlight Chats icon, 2 --> highlight nothing
...
window.addEventListener("locationChange", () => {
//location is the thing like "/post/postID/..."
if (window.location == "/chats") {
setAppBarIndex(1);
} else if (window.location == "/") {
setAppBarIndex(0);
} else {
setAppBarIndex(2);
}
})
Obviously I cannot use React in flutter, so I was finding for a similar easy way to do it on flutter.
How can I make the shared BottomBarWithRecord widget change as the route changes?
Oh man it's already 2AM ahhhh
Thx for reading this till here, and I gotta go sleep rn
If I've mad e any typo, just ignore them
You can define a root widget from which you'll control what screen should be displayed and position the screen and the BottomBar accordingly. So instead of having a Navigator() and BottomBar() inside your Stack, you'll have YourScreen() and BottomBar().
Scaffold(
body: SafeArea(
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: _buildScreen(screenIndex),
),
Align(
alignment: Alignment.bottomCenter,
child: BottomBar(
screenIndex,
onChange: (newIndex) {
setState(() {
screenIndex = newIndex;
});
},
),
),
],
),
),
)
BotttomBar will use the screenIndex passed to it to do what you had in mind and highlight the selected item.
_buildScreen will display the corresponding screen based on screenIndex and you pass the onChange to your BottomBar so that it can update the screen if another item was selected. You won't be using Navigator.of(context).push() in this case unless you want to route to a screen without the BottomBar. Otherwise the onChange passed to BottomBar will be responsible for updating the index and building the new screen.
This is how you could go about it if you wanted to implement it yourself. This package can do what you want as well. Here is a simple example:
class Dashboard extends StatefulWidget {
const Dashboard({Key? key}) : super(key: key);
#override
State<Dashboard> createState() => _DashboardState();
}
class _DashboardState extends State<Dashboard> {
final PersistentTabController _controller = PersistentTabController(initialIndex: 0);
#override
Widget build(BuildContext context) {
return PersistentTabView(
context,
controller: _controller,
screens: _buildScreens(),
items: _navBarsItems(),
);
}
List<Widget> _buildScreens() {
return [
const FirstScreen(),
const SecondScreen(),
];
}
List<PersistentBottomNavBarItem> _navBarsItems() {
return [
PersistentBottomNavBarItem(
icon: const Icon(Icons.home),
title: ('First Screen'),
),
PersistentBottomNavBarItem(
icon: const Icon(Icons.edit),
title: ('Second Screen'),
),
];
}
}
class FirstScreen extends StatelessWidget {
const FirstScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Center(
child: Text('First Screen'),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const Center(
child: Text('Second Screen'),
);
}
}

How can I change the child of a container via Dart code?

I'd like to dynamically set the child widget of a specific container using Dart code, specifically on a button press. Where should I start to accomplish this? I've already explored using a PageView, but that's not really what I'm looking for.
If I were web developing, I would easily be able to edit an element via its ID. Is there a Flutter equivalent of HTML ID's? Controllers seem limiting here.
Flutter doesn't really work that way. Flutter, like React, is a declarative environment as opposed to an imperative environment. Widget trees are built from state, and as such, a widget higher in the tree doesn't know, or even care, about its children.
https://flutter.dev/docs/get-started/flutter-for/declarative
https://flutter.dev/docs/get-started/flutter-for/web-devs
Try this
class MyHomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyHomePageState();
}
}
class _MyHomePageState extends State<MyHomePage> {
Widget _dynamicWidget = FlutterLogo();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_dynamicWidget,
FlatButton(
onPressed: () {
setState(() {
_dynamicWidget = Container(
color: Colors.red,
width: 50,
height: 50,
);
});
},
child: Text('Change')),
],
),
),
);
}
}