Android WebView 详解 2 androidwebview最新版
bigegpt 2024-10-12 05:15 7 浏览
[接上文]
// 多数情况下,可通过KeyChain.choosePrivateKeyAlias启动一个Activity供用户选择合适的私钥
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
request.cancel();
}
// 处理HTTP认证请求,默认行为是取消请求
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
handler.cancel();
}
// 通知应用有个已授权账号自动登陆了
public void onReceivedLoginRequest(WebView view, String realm, String account, String args) {
}
// 给应用一个机会处理按键事件
// 如果返回true,WebView不处理该事件,否则WebView会一直处理,默认返回false
public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
return false;
}
// 处理未被WebView消费的按键事件
// WebView总是消费按键事件,除非是系统按键或shouldOverrideKeyEvent返回true
// 此方法在按键事件分派时被异步调用
public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
super.onUnhandledKeyEvent(view, event);
}
// 通知应用页面缩放系数变化
public void onScaleChanged(WebView view, float oldScale, float newScale) {
}
WebChromeClient
// 获得所有访问历史项目的列表,用于链接着色。
public void getVisitedHistory(ValueCallback<String[]> callback) {
}
// <video /> 控件在未播放时,会展示为一张海报图,HTML中可通过它的'poster'属性来指定。
// 如果未指定'poster'属性,则通过此方法提供一个默认的海报图。
public Bitmap getDefaultVideoPoster() {
return null;
}
// 当全屏的视频正在缓冲时,此方法返回一个占位视图(比如旋转的菊花)。
public View getVideoLoadingProgressView() {
return null;
}
// 接收当前页面的加载进度
public void onProgressChanged(WebView view, int newProgress) {
}
// 接收文档标题
public void onReceivedTitle(WebView view, String title) {
}
// 接收图标(favicon)
public void onReceivedIcon(WebView view, Bitmap icon) {
}
// Android中处理Touch Icon的方案
// http://droidyue.com/blog/2015/01/18/deal-with-touch-icon-in-android/index.html
public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) {
}
// 通知应用当前页进入了全屏模式,此时应用必须显示一个包含网页内容的自定义View
public void onShowCustomView(View view, CustomViewCallback callback) {
}
// 通知应用当前页退出了全屏模式,此时应用必须隐藏之前显示的自定义View
public void onHideCustomView() {
}
// 显示一个alert对话框
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return false;
}
// 显示一个confirm对话框
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return false;
}
// 显示一个prompt对话框
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
return false;
}
// 显示一个对话框让用户选择是否离开当前页面
public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
return false;
}
// 指定源的网页内容在没有设置权限状态下尝试使用地理位置API。
// 从API24开始,此方法只为安全的源(https)调用,非安全的源会被自动拒绝
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
}
// 当前一个调用 onGeolocationPermissionsShowPrompt() 取消时,隐藏相关的UI。
public void onGeolocationPermissionsHidePrompt() {
}
// 通知应用打开新窗口
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
return false;
}
// 通知应用关闭窗口
public void onCloseWindow(WebView window) {
}
// 请求获取取焦点
public void onRequestFocus(WebView view) {
}
// 通知应用网页内容申请访问指定资源的权限(该权限未被授权或拒绝)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onPermissionRequest(PermissionRequest request) {
request.deny();
}
// 通知应用权限的申请被取消,隐藏相关的UI。
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onPermissionRequestCanceled(PermissionRequest request) {
}
// 为'<input type="file" />'显示文件选择器,返回false使用默认处理
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
return false;
}
// 接收JavaScript控制台消息
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
return false;
}
回调顺序
页面加载回调顺序:
shouldOverrideUrlLoading
onProgressChanged[10]
shouldInterceptRequest
onProgressChanged[...]
onPageStarted
onProgressChanged[...]
onLoadResource
onProgressChanged[...]
onReceivedTitle/onPageCommitVisible
onProgressChanged[100]
onPageFinished
onReceivedIcon
资源加载回调:
shouldInterceptRequest() -> onLoadResource()
发生重定向时回调:
onPageStarted() -> shouldOverrideUrlLoading()
直接loadUrl的回调:
// 无重定向
onPageStarted() -> onPageFinished()
// 有重定向,shouldOverrideUrlLoading 返回 true 时 onPageFinished 仍会执行
onPageStarted() -> redirection -> ... -> onPageFinished()
用户点击链接的回调:
// shouldOverrideUrlLoading 返回 true 时不执行onPageStarted/onPageFinished
shouldOverrideUrlLoading() -> ...
// 无重定向
shouldOverrideUrlLoading() -> onPageStarted() -> onPageFinished()
// 有重定向
shouldOverrideUrlLoading() -> onPageStarted() -> redirection -> ... -> onPageFinished()
后退/前进/刷新 时回调:
onPageStarted() -> onPageFinished()
关于 window.location
假设从A页面跳转到B页面
如果页面B中直接输出 window.location="http://example.com",那页面B不会被加入回退栈,回退将直接回到A页
如果页面B加载完成后,比如用setTimeout延迟了,那页面B会被加入回退栈,当回退到页面A时会再执行跳转,这会导致回退功能看起来不正常,需要快速回退两次才能回到A页面
视口(viewport)
https://developer.android.com/guide/webapps/targeting.html
https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag
https://developer.mozilla.org/zh-CN/docs/Web/CSS/@viewport
视口是一个为网页提供绘图区域的矩形。
你可以指定数个视口属性,比如尺寸和初始缩放系数(initial scale)。其中最重要的是视口宽度,它定义了网页水平方向的可用像素总数(可用的CSS像素数)。
多数 Android 上的网页浏览器(包括 Chrome)设置默认视口为一个大尺寸(被称为”wide viewport mode”,宽约 980px)。
也有许多浏览器默认会尽可能缩小以显示完整的视口宽度(被称为”overview mode“)。
// 是否支持viewport属性,默认值 false
// 页面通过`<meta name="viewport" ... />`自适应手机屏幕
// 当值为true且viewport标签不存在或未指定宽度时使用 wide viewport mode
settings.setUseWideViewPort(true);
// 是否使用overview mode加载页面,默认值 false
// 当页面宽度大于WebView宽度时,缩小使页面宽度等于WebView宽度
settings.setLoadWithOverviewMode(true);
viewport 语法
<meta name="viewport"
content="
height = [pixel_value | "device-height"] ,
width = [pixel_value | "device-width"] ,
initial-scale = float_value ,
minimum-scale = float_value ,
maximum-scale = float_value ,
user-scalable = ["yes" | "no"]
" />
指定视口宽度精确匹配设备屏幕宽度同时禁用了缩放
<head>
<title>Example</title>
<meta name="viewport" content="width=device-width, user-scalable=no" />
</head>
通过WebView设置初始缩放(initial-scale)
// 设置初始缩放百分比
// 0表示依赖于setUseWideViewPort和setLoadWithOverviewMode
// 100表示不缩放
web.setInitialScale(0)
管理 Cookies
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies
Cookie 是服务器发送到用户浏览器并保存在浏览器上的一块数据,它会在浏览器下一次发起请求时被携带并发送到服务器上。
可通过Cookie保存浏览信息来获得更轻松的在线体验,比如保持登录状态、记住偏好设置,并提供本地的相关内容。
会话Cookie 与 持久Cookie
会话cookie不需要指定Expires和Max-Age,浏览器关闭之后它会被自动删除。
持久cookie指定了Expires或Max-Age,会被存储到磁盘上,不会因浏览器而失效。
第一方Cookie 与 第三方Cookie
每个Cookie都有与之关联的域,与页面域一样的就是第一方Cookie,不一样的就是第三方Cookie。
// 设置接收第三方Cookie
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().setAcceptThirdPartyCookies(vWeb, true);
}
读取/写入/移除 Cookie
// 获取指定url关联的所有Cookie
// 返回值使用"Cookie"请求头格式:"name=value; name2=value2; name3=value3"
CookieManager.getInstance().getCookie(url);
// 为指定的url设置一个Cookie
// 参数value使用"Set-Cookie"响应头格式,参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie
CookieManager.getInstance().setCookie(url, value);
// 移除指定url下的指定Cookie
CookieManager.getInstance().setCookie(url, cookieName + "=");
webkit cookie 工具类
public class WebkitCookieUtil {
// 移除指定url关联的所有cookie
public static void remove(String url) {
CookieManager cm = CookieManager.getInstance();
for (String cookie : cm.getCookie(url).split("; ")) {
cm.setCookie(url, cookie.split("=")[0] + "=");
}
flush();
}
// sessionOnly 为true表示移除所有会话cookie,否则移除所有cookie
public static void remove(boolean sessionOnly) {
CookieManager cm = CookieManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (sessionOnly) {
cm.removeSessionCookies(null);
} else {
cm.removeAllCookies(null);
}
} else {
if (sessionOnly) {
cm.removeSessionCookie();
} else {
cm.removeAllCookie();
}
}
flush();
}
// 写入磁盘
public static void flush() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().flush();
} else {
CookieSyncManager.getInstance().sync();
}
}
}
同步系统Cookie 与 Webkit Cookie
// 将系统级Cookie(比如`new URL(...).openConnection()`的Cookie) 同步到 WebView
public class WebkitCookieHandler extends CookieHandler {
private static final String TAG = WebkitCookieHandler.class.getSimpleName();
private CookieManager wcm;
public WebkitCookieHandler() {
this.wcm = CookieManager.getInstance();
}
@Override
public void put(URI uri, Map<String, List<String>> headers) throws IOException {
if ((uri == null) || (headers == null)) {
return;
}
String url = uri.toString();
for (String headerKey : headers.keySet()) {
if ((headerKey == null) || !(headerKey.equalsIgnoreCase("set-cookie2") || headerKey.equalsIgnoreCase("set-cookie"))) {
continue;
}
for (String headerValue : headers.get(headerKey)) {
Log.e(TAG, headerKey + ": " + headerValue);
this.wcm.setCookie(url, headerValue);
}
}
}
@Override
public Map<String, List<String>> get(URI uri, Map<String, List<String>> headers) throws IOException {
if ((uri == null) || (headers == null)) {
throw new IllegalArgumentException("Argument is null");
}
String url = uri.toString();
String cookie = this.wcm.getCookie(url);
Log.e(TAG, "cookie: " + cookie);
if (cookie != null) {
return Collections.singletonMap("Cookie", Arrays.asList(cookie));
} else {
return Collections.emptyMap();
}
}
}
缓存(Cache)
设置缓存模式
WebSettings.LOAD_DEFAULT 根据cache-control决定是否从网络上取数据
WebSettings.LOAD_CACHE_ELSE_NETWORK 无网,离线加载,优先加载缓存(即使已经过期)
WebSettings.LOAD_NO_CACHE 仅从网络加载
WebSettings.LOAD_CACHE_ONLY 仅从缓存加载
// 网络正常时根据cache-control决定是否从网络上取数据
if (isNetworkConnected(mActivity)) {
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
} else {
settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}
清除缓存
// 传入true表示同时内存与磁盘,false表示仅清除内存
// 由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序
web.clearCache(true);
预加载(Preload)
一个简单的预加载示例(shouldInterceptRequest)
点击 assets/demo.xml 里的链接”hello”时会加载本地的 assets/hello.html
assets/demo.xml
<html>
<body>
<a href="http://demo.com/assets/hello.html">hello</a>
</body>
</html>
assets/hello.html
<html>
<body>
hello world!
</body>
</html>
重载 shouldInterceptRequest
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return preload("assets/", url);
}
WebResourceResponse preload(String path, String url) {
if (!url.contains(path)) {
return null;
}
String local = url.replaceFirst("^http.*" + path, "");
try {
InputStream is = getApplicationContext().getAssets().open(local);
String ext = MimeTypeMap.getFileExtensionFromUrl(local);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
return new WebResourceResponse(mimeType, "UTF-8", is);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
与Javascript交互
启用Javascript
// 是否支持Javascript,默认值false
settings.setJavaScriptEnabled(true);
注入对象到Javascript
// 注入对象'jsobj',在网页中通过`jsobj.say(...)`调用
web.addJavascriptInterface(new JSObject(), "jsobj")
在API17后支持白名单,只有添加了@JavascriptInterface注解的方法才会注入JS
public class JSObject {
@JavascriptInterface
public void say(String words) {
// todo
}
}
移除已注入Javascript的对象
1
web.removeJavascriptInterface("jsobj")
执行JS表达式
// 弹出提示框
web.loadUrl("javascript:alert('hello')");
// 调用注入的jsobj.say方法
web.loadUrl("javascript:jsobj.say('hello')");
在API19后可异步执行JS表达式,并通过回调返回值
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
vWeb.evaluateJavascript("111+222", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
// value => "333"
}
});
}
地理位置(Geolocation)
https://developer.mozilla.org/zh-CN/docs/Web/API/Geolocation/Using_geolocation
需要以下权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
默认可用
settings.setGeolocationEnabled(true);
当H5调用地理位置API时,会先通过WebChromeClient.onGeolocationPermissionsShowPrompt申请授权
// 指定源的网页内容在没有设置权限状态下尝试使用地理位置API。
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
boolean allow = true; // 是否允许origin使用定位API
boolean retain = false; // 内核是否记住这次制授权
callback.invoke(origin, true, false);
}
// 之前调用 onGeolocationPermissionsShowPrompt() 申请的授权被取消时,隐藏相关的UI。
@Override
public void onGeolocationPermissionsHidePrompt() {
}
注:从API24开始,仅支持安全源(https)的请求,非安全源的请求将自动拒绝且不调用 onGeolocationPermissionsShowPrompt 与 onGeolocationPermissionsHidePrompt
弹框(alert/confirm/prompt/onbeforeunload)
在javascript中使用 alert/confirm/prompt 会弹出对话框,可通过重载 WebChromeClient 的下列方法控制弹框的交互,比如替换系统默认的对话框或屏蔽这些对话框
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
// 这里处理交互逻辑
// result.cancel(); 表示用户取消了操作(点击了取消按钮)
// result.confirm(); 表示用户确认了操作(点击了确认按钮)
// ...
// 返回true表示自已处理,返回false表示由系统处理
return false;
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return false;
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
return false;
}
@Override
public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
return false;
}
全屏(Fullscreen)
Fullscreen API
https://developer.mozilla.org/zh-CN/docs/DOM/Using_fullscreen_mode
当H5请求全屏时,会回调 WebChromeClient.onShowCustomView 方法
当H5退出全屏时,会回调 WebChromeClient.onHideCustomView 方法
1.manifest
自己处理屏幕尺寸方向的变化(切换屏幕方向时不重建activity)
WebView播放视频需要开启硬件加速
<activity
android:name=".WebViewActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:screenOrientation="portrait" />
2.页面布局
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/Toolbar.Back"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/web"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
...
</FrameLayout>
</LinearLayout>
3.处理全屏回调
CustomViewCallback mCallback;
View vCustom;
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
setFullscreen(true);
vCustom = view;
mCallback = callback;
if (vCustom != null) {
ViewGroup parent = (ViewGroup) vWeb.getParent();
parent.addView(vCustom);
}
}
@Override
public void onHideCustomView() {
setFullscreen(false);
if (vCustom != null) {
ViewGroup parent = (ViewGroup) vWeb.getParent();
parent.removeView(vCustom);
vCustom = null;
}
if (mCallback != null) {
mCallback.onCustomViewHidden();
mCallback = null;
}
}
4.设置全屏,切换屏幕方向
void setFullscreen(boolean fullscreen) {
if (fullscreen) {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
vToolbar.setVisibility(View.GONE);
vWeb.setVisibility(View.GONE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
vToolbar.setVisibility(View.VISIBLE);
vWeb.setVisibility(View.VISIBLE);
}
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
内存泄漏
直接 new WebView 并传入 application context 代替在 XML 里面声明以防止 activity 引用被滥用,能解决90+%的 WebView 内存泄漏。
vWeb = new WebView(getContext().getApplicationContext());
container.addView(vWeb);
销毁 WebView
if (vWeb != null) {
vWeb.setWebViewClient(null);
vWeb.setWebChromeClient(null);
vWeb.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
vWeb.clearHistory();
((ViewGroup) vWeb.getParent()).removeView(vWeb);
vWeb.destroy();
vWeb = null;
}
相关推荐
- 最全的MySQL总结,助你向阿里“开炮”(面试题+笔记+思维图)
-
前言作为一名编程人员,对MySQL一定不会陌生,尤其是互联网行业,对MySQL的使用是比较多的。对于求职者来说,MySQL又是面试中一定会问到的重点,很多人拥有大厂梦,却因为MySQL败下阵来。实际上...
- Redis数据库从入门到精通(redis数据库设计)
-
目录一、常见的非关系型数据库NOSQL分类二、了解Redis三、Redis的单节点安装教程四、Redis的常用命令1、Help帮助命令2、SET命令3、过期命令4、查找键命令5、操作键命令6、GET命...
- netcore 急速接入第三方登录,不看后悔
-
新年新气象,趁着新年的喜庆,肝了十来天,终于发了第一版,希望大家喜欢。如果有不喜欢看文字的童鞋,可以直接看下面的地址体验一下:https://oauthlogin.net/前言此次带来得这个小项目是...
- 精选 30 个 C++ 面试题(含解析)(c++面试题和答案汇总)
-
大家好,我是柠檬哥,专注编程知识分享。欢迎关注@程序员柠檬橙,编程路上不迷路,私信发送以下关键字获取编程资源:发送1024打包下载10个G编程资源学习资料发送001获取阿里大神LeetCode...
- Oracle 12c系列(一)|多租户容器数据库
-
作者杨禹航出品沃趣技术Oracle12.1发布至今已有多年,但国内Oracle12C的用户并不多,随着12.2在去年的发布,选择安装Oracle12c的客户量明显增加,在接下来的几年中,Or...
- flutter系列之:UI layout简介(flutter-ui-nice)
-
简介对于一个前端框架来说,除了各个组件之外,最重要的就是将这些组件进行连接的布局了。布局的英文名叫做layout,就是用来描述如何将组件进行摆放的一个约束。在flutter中,基本上所有的对象都是wi...
- Flutter 分页功能表格控件(flutter 列表)
-
老孟导读:前2天有读者问到是否有带分页功能的表格控件,今天分页功能的表格控件详细解析来来。PaginatedDataTablePaginatedDataTable是一个带分页功能的DataTable,...
- Flutter | 使用BottomNavigationBar快速构建底部导航
-
平时我们在使用app时经常会看到底部导航栏,而在flutter中它的实现也较为简单.需要用到的组件:BottomNavigationBar导航栏的主体BottomNavigationBarI...
- Android中的数据库和本地存储在Flutter中是怎样实现的
-
如何使用SharedPreferences?在Android中,你可以使用SharedPreferencesAPI来存储少量的键值对。在Flutter中,使用Shared_Pref...
- Flet,一个Flutter应用的实用Python库!
-
▼Flet:用Python轻松构建跨平台应用!在纷繁复杂的Python框架中,Flet宛如一缕清风,为开发者带来极致的跨平台应用开发体验。它用最简单的Python代码,帮你实现移动端、桌面端...
- flutter系列之:做一个图像滤镜(flutter photo)
-
简介很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是h5页面,那么我们可以很容易的通过css滤镜来实现这个功能。那么如果在flutter中,如果要实现这样的滤镜功能应该怎么处理呢?一起...
- flutter软件开发笔记20-flutter web开发
-
flutterweb开发优势比较多,采用统一的语言,就能开发不同类型的软件,在web开发中,特别是后台式软件中,相比传统的html5开发,更高效,有点像c++编程的方式,把web设计出来了。一...
- Flutter实战-请求封装(五)之设置抓包Proxy
-
用了两年的flutter,有了一些心得,不虚头巴脑,只求实战有用,以供学习或使用flutter的小伙伴参考,学习尚浅,如有不正确的地方还望各路大神指正,以免误人子弟,在此拜谢~(原创不易,转发请标注来...
- 为什么不在 Flutter 中使用全局变量来管理状态
-
我相信没有人用全局变量来管理Flutter应用程序的状态。毫无疑问,我们的Flutter应用程序需要状态管理包或Flutter的基本小部件(例如InheritedWidget或St...
- Flutter 攻略(Dart基本数据类型,变量 整理 2)
-
代码运行从main方法开始voidmain(){print("hellodart");}变量与常量var声明变量未初始化变量为nullvarc;//未初始化print(c)...
- 一周热门
- 最近发表
-
- 最全的MySQL总结,助你向阿里“开炮”(面试题+笔记+思维图)
- Redis数据库从入门到精通(redis数据库设计)
- netcore 急速接入第三方登录,不看后悔
- 精选 30 个 C++ 面试题(含解析)(c++面试题和答案汇总)
- Oracle 12c系列(一)|多租户容器数据库
- flutter系列之:UI layout简介(flutter-ui-nice)
- Flutter 分页功能表格控件(flutter 列表)
- Flutter | 使用BottomNavigationBar快速构建底部导航
- Android中的数据库和本地存储在Flutter中是怎样实现的
- Flet,一个Flutter应用的实用Python库!
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- libcrypto.so (74)
- linux安装minio (74)
- ubuntuunzip (67)
- vscode使用技巧 (83)
- secure-file-priv (67)
- vue阻止冒泡 (67)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)