Flutter项目实战
flutter 数据请求(dio)
在pubspec.yaml,引入dio请求库
dio: ^4.0.6
初始化Dio
创建一个新的文件:dio_client.dart包含DioClient
class DioClient {
// TODO: Set up and define the methods for network operations
}
您可以使用以下方法初始化 Dio:
import 'package:dio/dio.dart';
class DioClient {
final Dio _dio = Dio();
}
定义 API 服务器的基本 URL:
import 'package:dio/dio.dart';
class DioClient {
final Dio _dio = Dio();
final _baseUrl = 'http://192.168.1.128';
// TODO: Add methods
}
[!note|labelVisibility:hidden|iconVisibility:hidden] 定义 GET 请求
Future<Page?> getBanner() async {
Page? res;
try {
Response response = await _dio.get(
"/content/page?siteId=1&channelIds=1752&page=1&size=5&typeIds=175",
);
LogUtil.v("结果是:${response.data}");
if (response.data['code'] == 200) {
res = Page.fromJson(response.data['data']);
}
} catch (e) {
LogUtil.e("Error: $e");
}
return res;
}
[!note|labelVisibility:hidden|iconVisibility:hidden] 定义 POST 请求
Future<Menu?> getMenu() async {
Menu? res;
try {
Response response = await _dio.post(
"/api/pldk-servicehall/homeHall/queryListColumn",
data: {'laborunCode': 1410, 'platform': 2});
LogUtil.v("结果是:${response.data}");
res = Menu.fromJson(response.data);
} catch (e) {
LogUtil.e("Error: $e");
}
return res;
}
[!note|style:flat|labelVisibility:hidden|iconVisibility:hidden] 选择和定义您的请求头
baseUrl
您可以在内部定义它BaseOptions
并在实例化时传递一次,而不是每次都传递端点Dio。
为此,您需要进行Dio
如下初始化:
final Dio _dio = Dio(
BaseOptions(
baseUrl: 'http://192.168.1.128',
connectTimeout: 5000,
receiveTimeout: 3000,
),
);
[!note|style:flat|labelVisibility:hidden|iconVisibility:hidden] 上传文件
Dio
使上传文件到服务器的过程变得更加简单。它可以同时处理多个文件上传,并有一个简单的回调来跟踪它们的进度,这使得它比http包更容易使用。
您可以使用FormDataDio
轻松地将文件上传到服务器。以下是向 API 发送图像文件的示例:
String imagePath;
FormData formData = FormData.fromMap({
"image": await MultipartFile.fromFile(
imagePath,
filename: "upload.jpeg",
),
});
Response response = await _dio.post(
'/search',
data: formData,
onSendProgress: (int sent, int total) {
print('$sent $total');
},
);
[!note|style:flat|labelVisibility:hidden|iconVisibility:hidden] 拦截器
您可以在使用then
处理 Dio 请求、响应错误之前拦截它们catchError
。在实际场景中,拦截器可用于使用JSON Web Tokens (JWT)进行授权、解析 JSON、处理错误以及轻松调试 Dio 网络请求。
您可以通过重写回调运行拦截:onRequest
,onResponse
,和onError
。
对于我们的示例,我们将定义一个简单的拦截器来记录不同类型的请求。创建一个名为Logging
从Interceptor
以下扩展的新类:
import 'package:dio/dio.dart';
import 'log_util.dart';
/// @author 田琪
/// class Logging
/// component : ${options.path}/Logging { ... }
/// comment : dio日志拦截系统
class Logging extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
LogUtil.v(
'REQUEST[${options.method}] => PATH: ${options.path} => HEADER: ${options.headers}',
);
super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
LogUtil.d(
'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}',
);
super.onResponse(response, handler);
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) {
LogUtil.e(
'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}',
);
super.onError(err, handler);
}
}
在这里,我们覆盖了由 Dio
请求触发的各种回调,并为每个回调添加了一个打印语句,用于在控制台中记录请求。
Dio在初始化期间添加拦截器:
final Dio _dio = Dio(
BaseOptions(
baseUrl: 'http://192.168.1.128',
connectTimeout: 5000,
),
)..interceptors.add(Logging());
dart实例类快速生成
[!Tip|style:flat] 根据 JSON 快速生成 Dart 的 bean 类的网址JSON to Dart
打开网址,把json文件复制进入输入框,然后输入类名,点击Generate Dart
快速生成实体类
项目实战案例
构建我们自己的日志系统
import 'dart:developer';
/// Log Util.
class LogUtil {
static const String _defTag = 'common_utils';
static bool _debugMode = false; //是否是debug模式,true: log v 不输出.
static int _maxLen = 128;
static String _tagValue = _defTag;
static void init({
String tag = _defTag,
bool isDebug = false,
int maxLen = 128,
}) {
_tagValue = tag;
_debugMode = isDebug;
_maxLen = maxLen;
}
static void d(Object? object, {String? tag}) {
if (_debugMode) {
log('$tag d | ${object?.toString()}');
}
}
static void e(Object? object, {String? tag}) {
_printLog(tag, ' e ', object);
}
static void v(Object? object, {String? tag}) {
if (_debugMode) {
_printLog(tag, ' v ', object);
}
}
static void _printLog(String? tag, String stag, Object? object) {
String da = object?.toString() ?? 'null';
tag = tag ?? _tagValue;
if (da.length <= _maxLen) {
// ignore: avoid_print
print('$tag$stag $da');
return;
}
// ignore: avoid_print
print(
'$tag$stag — — — — — — — — — — — — — — — — st — — — — — — — — — — — — — — — —');
while (da.isNotEmpty) {
if (da.length > _maxLen) {
// ignore: avoid_print
print('$tag$stag| ${da.substring(0, _maxLen)}');
da = da.substring(_maxLen, da.length);
} else {
// ignore: avoid_print
print('$tag$stag| $da');
da = '';
}
}
// ignore: avoid_print
print(
'$tag$stag — — — — — — — — — — — — — — — — ed — — — — — — — — — — — — — — — —');
}
}
[!note|style:flat|labelVisibility:hidden|iconVisibility:hidden] 在 main.dart 的 main 方法中初始化日志
void main() {
// 初始化日志系统
LogUtil.init(tag: "package:yuncheng-utils", isDebug: true);
runApp(const Myapp());
}
调用jecms的接口开始渲染轮播图
上文中的GET方法返回了cms请求的banner的列表
- page.dart
import 'banner.dart';
class Page {
List<Banner>? _content;
bool? _first;
bool? _last;
int? _size;
int? _totalElements;
int? _totalPages;
Page(
{List<Banner>? content,
bool? first,
bool? last,
int? size,
int? totalElements,
int? totalPages}) {
if (content != null) {
this._content = content;
}
if (first != null) {
this._first = first;
}
if (last != null) {
this._last = last;
}
if (size != null) {
this._size = size;
}
if (totalElements != null) {
this._totalElements = totalElements;
}
if (totalPages != null) {
this._totalPages = totalPages;
}
}
List<Banner>? get content => _content;
set content(List<Banner>? content) => _content = content;
bool? get first => _first;
set first(bool? first) => _first = first;
bool? get last => _last;
set last(bool? last) => _last = last;
int? get size => _size;
set size(int? size) => _size = size;
int? get totalElements => _totalElements;
set totalElements(int? totalElements) => _totalElements = totalElements;
int? get totalPages => _totalPages;
set totalPages(int? totalPages) => _totalPages = totalPages;
Page.fromJson(Map<String, dynamic> json) {
if (json['content'] != null) {
_content = <Banner>[];
json['content'].forEach((v) {
_content!.add(new Banner.fromJson(v));
});
}
_first = json['first'];
_last = json['last'];
_size = json['size'];
_totalElements = json['totalElements'];
_totalPages = json['totalPages'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
if (this._content != null) {
data['content'] = this._content!.map((v) => v.toJson()).toList();
}
data['first'] = this._first;
data['last'] = this._last;
data['size'] = this._size;
data['totalElements'] = this._totalElements;
data['totalPages'] = this._totalPages;
return data;
}
}
- lib\bean\banner.dart
class Banner {
int? _id;
String? _title;
String? _txt;
String? _releaseTime;
String? _iconUrl;
Banner(
{int? id,
String? title,
String? txt,
String? releaseTime,
String? iconUrl}) {
if (id != null) {
this._id = id;
}
if (title != null) {
this._title = title;
}
if (txt != null) {
this._txt = txt;
}
if (releaseTime != null) {
this._releaseTime = releaseTime;
}
if (iconUrl != null) {
this._iconUrl = iconUrl;
}
}
int? get id => _id;
set id(int? id) => _id = id;
String? get title => _title;
set title(String? title) => _title = title;
String? get txt => _txt;
set txt(String? txt) => _txt = txt;
String? get releaseTime => _releaseTime;
set releaseTime(String? releaseTime) => _releaseTime = releaseTime;
String? get iconUrl => _iconUrl;
set iconUrl(String? iconUrl) => _iconUrl = iconUrl;
Banner.fromJson(Map<String, dynamic> json) {
_id = json['id'];
_title = json['title'];
_txt = json['txt'];
_releaseTime = json['releaseTime'];
_iconUrl = json['iconUrl'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this._id;
data['title'] = this._title;
data['txt'] = this._txt;
data['releaseTime'] = this._releaseTime;
data['iconUrl'] = this._iconUrl;
return data;
}
}
准备工作已完成,开始页面的编写
- lib\tabbar\home_screen.dart
import 'package:cached_network_image/cached_network_image.dart';
import 'package:card_swiper/card_swiper.dart';
import 'package:flutter/material.dart';
import '../utils/dio_client.dart';
import '../bean/page.dart' as bp;
import '../bean/banner.dart' as bank;
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final DioClient _client = DioClient();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("临汾市总工会"),
),
drawer: Drawer(
child: Column(children: [
DrawerHeader(
decoration: const BoxDecoration(
color: Colors.white,
),
padding: const EdgeInsets.all(0),
margin: const EdgeInsets.all(0),
child: UserAccountsDrawerHeader(
margin: const EdgeInsets.all(0),
currentAccountPicture: ClipOval(
child: Image.network(
"https://pic.tianqinote.com/classroom_1.png",
fit: BoxFit.fill,
),
),
currentAccountPictureSize: const Size.square(72.0),
accountName: const Text(
"田琪",
style: TextStyle(color: Colors.black),
),
accountEmail: const Text(
"[email protected]",
style: TextStyle(color: Colors.black),
),
decoration: const BoxDecoration(
image: DecorationImage(
image: NetworkImage("https://pic.tianqinote.com/loading.png"),
fit: BoxFit.cover,
),
),
),
),
const ListTile(
leading: CircleAvatar(
child: Icon(Icons.settings),
),
title: Text("设置"),
),
const ListTile(
leading: CircleAvatar(
child: Icon(Icons.person),
),
title: Text("个人"),
),
const ListTile(
leading: CircleAvatar(
child: Icon(Icons.edit),
),
title: Text("修改密码"),
)
]),
),
body: Padding(
padding: const EdgeInsets.all(0.0),
child: FutureBuilder(
future: _client.getBanner(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
bp.Page? res = snapshot.data;
if (res != null) {
return _buildSwiper(res.content);
}
}
return const CircularProgressIndicator();
},
),
),
);
}
Widget _buildSwiper(List<bank.Banner>? list) {
return SizedBox(
width: 360.0,
height: 200.0,
child: Swiper(
itemBuilder: (context, index) {
return CachedNetworkImage(
imageUrl: DioClient.myBaseUrl + list![index].iconUrl.toString(),
placeholder: (context, url) => Image.network(
"https://pic.tianqinote.com/loading.png",
fit: BoxFit.cover,
),
errorWidget: (context, url, error) =>
const Icon(Icons.image_not_supported),
fit: BoxFit.cover,
);
},
itemCount: list!.length,
pagination: const SwiperPagination(),
autoplay: true,
onTap: (index) {
Navigator.pushNamed(context, "/web",
arguments: {'txt': list[index].txt, 'title': list[index].title});
},
),
);
}
}
这里边我们使用了Swiper
组件,我们单独介绍一下card_swiper组件
card_swiper组件
- 首先在pubspec.yaml文件中引入card_swiper
card_swiper: ^2.0.4
基础
参数 | 默认值 | 说明 |
---|---|---|
scrollDirection | Axis.horizontal | 如果是 Axis.horizontal , 滚动视图的子视图水平排列在一行中,而不是垂直排列在一列中。 |
axisDirection | AxisDirection.left | 如果是 AxisDirection.right , 滚动视图的子视图按行排列在右侧,而不是左侧。注意:当前仅支持水平堆栈布局 |
loop | true | 设置为 false 可禁用连续循环模式。 |
index | 0 | 初始幻灯片的索引号。 |
autoplay | false | 设置为 true 启用自动播放模式。 |
onIndexChanged | void onIndexChanged(int index) | 用户刷卡或自动播放时使用新索引调用 |
onTap | void onTap(int index) | 当用户点击ui时调用。 |
duration | 300.0 | 每笔交易动画成本的milliscends |
pagination | null | 设置 SwiperPagination() 以显示默认分页 |
control | null | 设置 SwiperControl() 以显示默认控件按钮 |
分页码
参数 | 默认值 | 说明 |
---|---|---|
alignment | Alignment.bottomCenter | 如果要将分页置于其他位置,请更改此值 |
margin | const EdgeInsets.all(10.0) | 父容器内侧之间的距离。 |
builder | SwiperPagination.dots | 有三种默认的样式SwiperPagination.dots, SwiperPagination.fraction and SwiperPagination.rect,这些可以定制。 |
如果您想自定义自己的分页,可以这样做:
Swiper(
...,
pagination: SwiperCustomPagination(
builder:(BuildContext context, SwiperPluginConfig config){
return YourOwnPagination();
}
)
);
实现分页
DotSwiperPaginationBuilder
参数 | 默认值 | 说明 | 是否必传 |
---|---|---|---|
activeColor | Theme.of(context).primaryColor | Active bullet color | false |
color | Theme.of(context).scaffoldBackgroundColor | Bullet | color false |
activeSize | 10.0 | Active bullet size | false |
size | 10.0 | Bullet size | false |
space | 3.0 | Distance between bullets | false |
key | - | key | false |
FractionPaginationBuilder
参数 | 默认值 | 说明 | 是否必传 |
---|---|---|---|
activeColor | Theme.of(context).primaryColor | Active font color | false |
color | Theme.of(context).scaffoldBackgroundColor | font color | false |
activeFontSize | 35.0 | Active font size | false |
fontSize 20.0 | Font | size | false |
key | - | key | false |
RectSwiperPaginationBuilder
参数 | 默认值 | 说明 | 是否必传 |
---|---|---|---|
activeColor | Theme.of(context).primaryColor | Active bullet color | false |
color | Theme.of(context).scaffoldBackgroundColor | Bullet color | false |
activeSize | 10.0 | Active bullet size | false |
size | 10.0 | Bullet size | false |
space | 3.0 | Distance between bullets | false |
key | - | key | false |
Control buttons
控件也从SwiperPlugin扩展,设置SwiperControl()以显示默认控件按钮。
参数 | 默认值 | 说明 |
---|---|---|
iconPrevious | Icons.arrow_back_ios | 显示上一个控制按钮的图标数据 |
iconNext | Icons.arrow_forward_ios | 接下来要显示的图标数据。 |
color | Theme.of(context).primaryColor | 控制按钮颜色 |
disableColor | Theme.of(context).disabledColor | 禁用控制按钮颜色 |
size | 30.0 | 控制按钮大小 |
padding | const EdgeInsets.all(5.0) | 控制按钮填充 |
Controller
控制器用于控制刷卡器的索引,启动或停止自动播放。您可以通过SwipController()创建控制器,并通过进一步使用保存实例。
方法 | 说明 |
---|---|
void move(int index, {bool animation: true}) | 移动到指定索引,是否带有动画 |
void next({bool animation: true}) | 下一个 |
void previous({bool animation: true}) | 上一个 |
void startAutoplay() | 开始自动播放 |
void stopAutoplay() | 停止自动播放 |
Autoplay
参数 | 默认值 | 说明 |
---|---|---|
autoplayDelay | 3000 | 自动播放延迟毫秒。 |
autoplayDisableOnInteraction | true | 如果设置为true,则在使用滑动时禁用自动播放。 |
内置布局
Swiper(
itemBuilder: (BuildContext context, int index) {
return Image.network(
"https://via.placeholder.com/288x188",
fit: BoxFit.fill,
);
},
itemCount: 10,
viewportFraction: 0.8,
scale: 0.9,
)
Swiper(
itemBuilder: (BuildContext context, int index) {
return Image.network(
"https://via.placeholder.com/288x188",
fit: BoxFit.fill,
);
},
itemCount: 10,
itemWidth: 300.0,
layout: SwiperLayout.STACK,
)
Swiper(
itemBuilder: (BuildContext context, int index) {
return Image.network(
"https://via.placeholder.com/288x188",
fit: BoxFit.fill,
);
},
itemCount: 10,
itemWidth: 300.0,
itemHeight: 400.0,
layout: SwiperLayout.TINDER,
)
非常容易创建您自己的自定义动画:
Swiper(
layout: SwiperLayout.CUSTOM,
customLayoutOption: CustomLayoutOption(
startIndex: -1,
stateCount: 3
)..addRotate([
-45.0/180,
0.0,
45.0/180
])..addTranslate([
Offset(-370.0, -40.0),
Offset(0.0, 0.0),
Offset(370.0, -40.0)
]),
itemWidth: 300.0,
itemHeight: 200.0,
itemBuilder: (context, index) {
return Container(
color: Colors.grey,
child: Center(
child: Text("$index"),
),
);
},
itemCount: 10,
)
CustomLayoutOption 用于描述动画,很容易在Swiper中指定项目的每个状态。
CustomLayoutOption(
// Which index is the first item of array below
startIndex: -1,
// array length
stateCount: 3
)..addRotate([
// rotation of every item
-45.0/180,
0.0,
45.0/180
])..addTranslate([
// offset of every item
Offset(-370.0, -40.0),
Offset(0.0, 0.0),
Offset(370.0, -40.0)
]);
MD5 字符串加密
- 首先在pubspec.yaml文件中引入crypto
crypto: ^3.0.2
字符串md5加密,转出之后需要使用 .toString() 转为String类型
import 'dart:convert';
var cont = const Utf8Encoder().convert(pwd);
var md5pwd = md5.convert(cont);
SM4 国密加密 dart版本
- 首先在pubspec.yaml文件中引入sm_crypto
sm_crypto: ^1.0.3
SM4.encrypt() 必传两个参数data(json字符串),key(后台加密数据)
Future<Person?> login(tel, pwd) async {
Person? res;
try {
var cont = const Utf8Encoder().convert(pwd);
var md5pwd = md5.convert(cont);
Map<String, String> datas = {
"name": tel, //账号
"pwd": md5pwd.toString(), //密码
};
String key = "980U9OCE9D565E7284052240B72EFC2E";
var encryptData = SM4.encrypt(
data: jsonEncode(datas),
key: key,
);
LogUtil.v("encryptData: $encryptData");
Response response = await _dio.post(
"/api/blade-auth/oauth/token?pldk_encrypt=$encryptData",
options: Options(headers: {
"Content-Type": "application/x-www-form-urlencoded",
}));
res = Person.fromJson(response.data);
} catch (e) {
LogUtil.e("Error: $e");
}
return res;
}
接下来我们可以新建登录页面了
- login.dart
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:yuncheng/bean/person.dart';
import 'package:yuncheng/utils/log_util.dart';
import '../../utils/dio_client.dart';
class Login extends StatefulWidget {
const Login({Key? key}) : super(key: key);
@override
_LoginState createState() => _LoginState();
}
class _LoginState extends State<Login> {
final DioClient _client = DioClient();
///用户名使用
late final TextEditingController _nameController = TextEditingController();
///密码输入框使用
late final TextEditingController _passwordController =
TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
child: Stack(
children: [
//第一层 背景图片
buildFunction1(),
//第二层 高斯模糊
buildFunction2(),
//第三层 登录输入层
buildFunction3(),
],
),
),
);
}
Widget buildFunction1() {
return Positioned.fill(
child: Image.asset(
"assets/images/bg.jpeg",
fit: BoxFit.cover,
),
);
}
Widget buildFunction2() {
return Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: Container(
color: Colors.white.withOpacity(0.4),
),
),
);
}
void _login(String name, String password) async {
Person? user = await _client.login(name, password);
LogUtil.v("user是 ${user?.toJson()}");
SharedPreferences sp = await SharedPreferences.getInstance();
if (user!.t != null) {
sp.setString("token", user.t!);
}
// ignore: use_build_context_synchronously
Navigator.of(context).pop();
// ignore: use_build_context_synchronously
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text("登录成功")));
}
Widget buildFunction3() {
return Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 300,
child: TextField(
controller: _nameController,
decoration: const InputDecoration(
hintText: "请输入用户名",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(33)),
borderSide: BorderSide(
color: Colors.grey,
),
),
contentPadding: EdgeInsets.all(10.0),
),
),
),
const SizedBox(height: 20),
SizedBox(
width: 300,
child: TextField(
controller: _passwordController,
textAlignVertical: TextAlignVertical.center,
decoration: const InputDecoration(
hintText: "请输入密码",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(33)),
borderSide: BorderSide(
color: Colors.grey,
),
),
contentPadding: EdgeInsets.all(10.0),
filled: true,
),
obscureText: true,
),
),
const SizedBox(height: 40),
SizedBox(
width: 300,
height: 48,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(33)),
child: ElevatedButton(
onPressed: () {
String name = _nameController.text;
String password = _passwordController.text;
LogUtil.v("获取到的内容是 $name $password");
_login(name, password);
},
child: const Text(
"登录",
style: TextStyle(
fontSize: 17,
),
),
),
),
)
],
),
);
}
}
flutter 富文本展示
- 首先在pubspec.yaml文件中引入webview_flutter
webview_flutter: ^3.0.4
新建展示webview的页面,简单展示我们获取自cms的内容
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:yuncheng/utils/log_util.dart';
class CommonWebview extends StatefulWidget {
final arguments;
const CommonWebview({Key? key, this.arguments}) : super(key: key);
@override
_CommonWebviewState createState() =>
_CommonWebviewState(arguments: arguments);
}
class _CommonWebviewState extends State<CommonWebview> {
final arguments;
_CommonWebviewState({this.arguments});
Future<void> _onNavigationDelegateExample(
WebViewController controller, BuildContext context) async {
final String contentBase64 =
base64Encode(const Utf8Encoder().convert("""<!DOCTYPE html>
<html>
<head><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
<body style='"margin: 0; padding: 0;'>
<div>
${arguments["txt"]}
</div>
</body>
</html>"""));
await controller.loadUrl('data:text/html;base64,$contentBase64');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("${arguments["title"]}")),
body: WebView(
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (controller) {
_onNavigationDelegateExample(controller, context);
},
),
);
}
}