warn: multiple substitutions specified in non-positional format; did you mean to add the formatted="false" attribute?

编辑`strings.xml`的时候

<string name="myurl">http://code.dd.com/rr?q=%rr.55</string>

或者

<string name="message_content'>恭喜您你获得%1s 积分,占比%2s %</string>

提示下面的错误

Multiple annotations found at this line:
- error: Multiple substitutions specified in non-positional format; did you mean to add
the formatted="false" attribute?
- error: Unexpected end tag string

出现这个错误的原因主要是因为`strings`字串中包含百分号`(%)`,

有几种方式解决

1.用两个百分号表示一个百分号即
<string name="myurl">http://code.dd.com/rr?q=%%rr.55</string>

<string name="message_content">恭喜您你获得%1s 积分,占比%2s %%</string>
2.用转义符表示
<string name="myurl">http://code.dd.com/rr?q=\%rr.55</string>
二:无需格式化

根据错误提示可知,如果字符串无需格式化,可在`<string>` 标签上增加属性:`formatted="false"`,即

<string name="myurl" formatted="false">http://code.dd.com/rr?q=%rr.55</string>
三:关于上面问题的延伸

如果你需要使用`String.format(String, Object...) `来格式化你的字符串,你可以把格式化参数放在你的字符串中,参见下面的例子:

<string name="welcome_messages">Hello, %1$s! You have %2$d new messages.</string>

在这个例子中,这个格式化的字符串有`2`个参数,` %1$s`是个字符串`%2$d`是个浮点数,你可以在你的程序中按照下面的方法来根据参数来格式化字符串:

Resources res = getResources();

String text = String.format(res.getString(R.string.welcome_messages), username, mailCount);

目前`Android`的`SDK`已经支持直接格式化字符串,不需要使用`String.format(String, Object...) `,如下

Resources res = getResources();

String text = res.getString(R.string.welcome_messages, username, mailCount);

那么根据例子上说的我需要把`%s`换成`%1$s`才行了,修改后编译通过,程序成功启动。

参考链接


Robolectric单元测试报错“org.mockito.exceptions.base.MockitoException Caused by: java.lang.ClassCastException”

使用`Robolectric`进行`Android`代码测试的时候,随着测试用例的增多,可能会报告如下错误(`Windows`下常见):

> Task :biz-h5:testDebugUnitTest

com.xxxx.plugin.face.FaceTest > executeAsync_Success FAILED
    org.mockito.exceptions.base.MockitoException at FaceTest.java:71
        Caused by: java.lang.ClassCastException at FaceTest.java:71

原因为`Mockto`使用了编译缓存导致加载类的时候出现异常。解决方法是禁止`Mockto`缓存测试类的代码。

在`Android`测试项目的`src/test/java`下创建一个名为`org.mockito.configuration`的包,然后实现一个名为`MockitoConfiguration.java`的类,如下:

package org.mockito.configuration;

public class MockitoConfiguration extends DefaultMockitoConfiguration {

  @Override
  public boolean enableClassCache() {
    return false;
  }
}

这样当再次执行测试用例的时候,就已经不使用缓存了。

参考链接


Android WebView/X5截长图解决方案

  1. 普通`WebView`如何截取长图
  2. 针对`X5内核中WebView`如何截取长图

日常开发中,遇到为`WebView`截取长图算是一种常见的需求。网上聪明的程序员们提供了多种截取`WebView`长图的方法,这为我们的开发提供了很多便利。现在,也有很多APP是集成了X5内核的,网上对于X5内核的截长图方案介绍比较少,所以这里我整理了对`WebView`截取长图的比较通用可行的方法,并且对使用了x5内核的`WebView`的截图方法进行分享。

普通`WebView`截长图方案

普通`WebView`截取长图,这里是指项目中没有集成X5内核的情况。利用`Google`文档上的api可以顺利截图。以`Android5.0`为版本分界线,截图采用不同的处理方式。

1. Android5.0以下版本

    /**
     * 对WebView进行截屏,虽然使用过期方法,但在当前Android版本中测试可行
     *
     * @param webView
     * @return
     */
    private static Bitmap captureWebViewKitKat(WebView webView) {
            Picture picture = webView.capturePicture();
            int width = picture.getWidth();
            int height = picture.getHeight();
            if (width > 0 && height > 0) {
                Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
                Canvas canvas = new Canvas(bitmap);
                picture.draw(canvas);
                return bitmap;
            }
            return null;
        }
    }

2. Android5.0及以上版本

在Android5.0及以上版本,Android对`WebView`进行了优化,为了减少内存使用和提高性能,使用`WebView`加载网页时只绘制显示部分。如果我们不做处理,仍然使用上述代码截图的话,就会出现只截到屏幕内显示的`WebView`内容,其它部分是空白的情况。
这时候,我们通过调用`WebView.enableSlowWholeDocumentDraw()`方法可以关闭这种优化,但要注意的是,该方法需要在`WebView`实例被创建前就要调用,否则没有效果。

另外这个方法一旦开启,会影响到整个进程中的`WebView`实例,并且没有办法关闭。

这个代码的本质是设置了一个全局变量,并且没有提供关闭接口。其真实调用的代码如下:

     private static boolean sRecordWholeDocumentEnabledByApi = false;
     static void enableSlowWholeDocumentDraw() {
          sRecordWholeDocumentEnabledByApi = true;
     }

我们在`WebView`实例被创建前加入代码:

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        android.webkit.WebView.enableSlowWholeDocumentDraw();
    }

另外,当应用存在多个进程的时候,比如消息推送进程,LBS定位进程存在的情况下,务必确保只在主进程中初始化这个设置,否则运行时可能报错。

根据`Google`文档中描述,`capturePicture()`方法已不鼓励使用,推荐我们通过`webView`的`onDraw(Canvas)`去获取图像,所以这里我们去拿到网页的宽高后,就调用`webView.draw(Canvas)`方法生成`webView`截图。

    private void captureWebViewLollipop(WebView webView) {
        float scale = webView.getScale();
        int width = webView.getWidth();
        int height = (int) (webView.getContentHeight() * scale + 0.5);
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
        Canvas canvas = new Canvas(bitmap);
        webView.draw(canvas);
        return bitmap;
    }

X5内核截取长图

使用X5内核截取长图有两种方法,并且都可以不用考虑版本问题,这为我们提供了方便。在X5内核下,如果使用`WebView`的`onDraw(Canvas)`方法,会出现或多或少的问题,所以对这个方法弃坑了。以下是两个截图方法:

1. 使用X5内核方法`snapshotWholePage(Canvas, boolean, boolean)`

在`X5`内核中提供了一个截取整个`WebView`界面的方法`snapshotWholePage(Canvas, boolean, boolean)`,但是这个方法有个缺点,就是不以屏幕上`WebView`的宽高截图,只是以`WebView`的`contentWidth`和`contentHeight`为宽高截图,所以截出来的图片会不怎么清晰,但作为缩略图效果还是不错了。

    private static Bitmap captureX5WebViewUnsharp(Context context, WebView webView) {
        if (webView == null) {
            return null;
        }
        if (context == null) {
            context = webView.getContext();
        }
        int width = webView.getContentWidth();
        int height = webView.getContentHeight();
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
        Canvas canvas = new Canvas(bitmap);
        webView.getX5WebViewExtension().snapshotWholePage(canvas, false, false);
        return bitmap;
    }

2. 使用`capturePicture()`截取清晰长图

如果想要在`X5`内核下截到清晰的长图,不能使用`snapshotWholePage()`,依然可以采用`capturePicture()`。X5内核下使用`capturePicture()`进行截图,可以直接拿到`WebView`的清晰长图,但这是个`Deprecated`的方法,使用的时候要做好异常处理。

总结

以上是`WebView`截长图方法的总结和分享,对X5内核的截图也是尝试了多种途径最后找到满意的解决方案。另外,截长图会占用大量内存,容易触发OOM,所以代码中也要注意对OOM的处理。

在使用了`X5`内核的项目中,使用`WebView`截取长图的判断逻辑可以是:

// 有x5内核没有生效,并且Android版本是5.0及以上时,调用enableSlowWholeDocumentDraw()方便截取长图
    if (!isX5Enabled() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        android.webkit.WebView.enableSlowWholeDocumentDraw();
    }
/* 创建WebView ×/

...

// 网页截图
    public void captureWholePage() {
        try {
            Bitmap bitmap = captureWebView();
            /* 对拿到的bitmap根据需要进行处理 */
        } catch (OutOfMemoryError oom) {
            /* 对OOM做处理
        }
    }

目前(`2020/08/01`)之前版本的`X5 SDK`,如果编译`APK`的时候指定`targetSdkVersion`版本高于 `28`(`Android O`)的情况下,调用`snapshotWholePage(Canvas, boolean, boolean)`可能会无法获取到截图,图片内容全黑。

观察日志发生如下报错:

2020-07-31 17:14:18.538 16536-16536/com.xxxx.xxx W/com.xxxx.xxx: Accessing hidden field Landroid/graphics/Canvas;->mBitmap:Landroid/graphics/Bitmap; (greylist-max-p, reflection, denied)
2020-07-31 17:14:18.539 16536-16536/com.xxxx.xxx W/System.err: java.lang.NoSuchFieldException: No field mBitmap in class Landroid/graphics/Canvas; (declaration of 'android.graphics.Canvas' appears in /system/framework/framework.jar)
2020-07-31 17:14:18.540 16536-16536/com.xxxx.xxx W/System.err:     at java.lang.Class.getDeclaredField(Native Method)
2020-07-31 17:14:18.540 16536-16536/com.xxxx.xxx W/System.err:     at com.tencent.smtt.os.SMTTAdaptation.a(TbsJavaCore:115)
2020-07-31 17:14:18.540 16536-16536/com.xxxx.xxx W/System.err:     at com.tencent.smtt.webkit.WebViewChromiumExtension.a(TbsJavaCore:2519)
2020-07-31 17:14:18.540 16536-16536/com.xxxx.xxx W/System.err:     at com.tencent.tbs.core.webkit.tencent.TencentWebViewProxy.snapshotWholePage(TbsJavaCore:2366)
2020-07-31 17:14:18.540 16536-16536/com.xxxx.xxx W/System.err:     at com.tencent.tbs.core.webkit.adapter.X5WebViewAdapter.snapshotWholePage(TbsJavaCore:851)

原因为从`API 29`(`Android P`)开始,`Google`对于某些反射调用私有方法的行为进行了限制,比如动态反射赋值`android.graphics.Canvas.java`的私有变量`mBitmap`。这些调用会被抛出异常阻止。

参考链接


AsyncTask is Deprecated, Now What?

For the past decade, AysncTask has been one of the most widely used solutions for writing concurrent code in Android. However, it earned very controversial reputation. On the one hand, AsyncTask powered, and still powers, many Android applications. On the other hand, most professional Android developers openly dislike this API.

All in all, I’d say that Android community has love-hate relationship with AsyncTask. But there are big news: the era of AsyncTask is about to end because a commit that deprecated it had just landed in Android Open Source Project.

In this post I’ll review the official statement motivating AsyncTask’s deprecation, as well as the real reasons why it had to be deprecated. As you’ll see, these are different sets of reasons. In addition, towards the end of this article, I’ll share my thoughts on the future of Android’s concurrency APIs.

Official Reason for Deprecation of AsyncTask

The official deprecation of AsyncTask, as well as the motivation for that decision, were introduced with this commit. The newly added first paragraph of Javadoc states:

AsyncTask was intended to enable proper and easy use of the UI thread. However, the most common use case was for integrating into UI, and that would cause Context leaks, missed callbacks, or crashes on configuration changes. It also has inconsistent behavior on different versions of the platform, swallows exceptions from doInBackground, and does not provide much utility over using Executors directly.

While that’s the official statement by Google, there are several inaccuracies there which are worth pointing out.

Fist of all, AsyncTask has never been intended to “enable proper and easy use of the UI thread”. It was intended to offload long-running operations from UI thread to background threads, and then deliver the results of these operations back to UI thread. I know, I’m nitpicking here. However, in my opinion, when Google deprecates API that they themselves invented and promoted for years, it would be more respectful towards developers who use this API today, and will continue to use for years to come, to invest more effort into deprecation message to prevent further confusion.

That said, the more interesting part of this deprecation message is this: “that would cause Context leaks, missed callbacks, or crashes on configuration changes”. So, Google basically states that the most common use case for AsyncTask automatically results in very serious problems. However, there are many high-quality applications out there which use AsyncTask and work flawlessly. Even some classes inside AOSP itself use AsyncTask. How comes they don’t experience these problems?

To answer this question, let’s discuss the relationship between AsyncTask and memory leaks in details.

继续阅读AsyncTask is Deprecated, Now What?

通过Chrome浏览器进行Android调试/Remote Debugging on Android with Chrome

The way your web content behaves on mobile can be dramatically different from the desktop experience. Remote debugging with Chrome DevTools lets you

继续阅读通过Chrome浏览器进行Android调试/Remote Debugging on Android with Chrome

Android最简单的HTTP服务器

目前在对Android的代码进行功能测试的时候,需要服务器返回一个数据来测试整个流程是否正确。不希望引入第三方的JAR包,因此需要一个特别简单的HTTP服务器。

网上查询了一下,找到可用的代码如下:

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.StringTokenizer;

// The tutorial can be found just here on the SSaurel's Blog : 
// https://www.ssaurel.com/blog/create-a-simple-http-web-server-in-java
// Each Client Connection will be managed in a dedicated Thread
public class JavaHTTPServer implements Runnable{ 
	
	static final File WEB_ROOT = new File(".");
	static final String DEFAULT_FILE = "index.html";
	static final String FILE_NOT_FOUND = "404.html";
	static final String METHOD_NOT_SUPPORTED = "not_supported.html";
	// port to listen connection
	static final int PORT = 8080;
	
	// verbose mode
	static final boolean verbose = true;
	
	// Client Connection via Socket Class
	private Socket connect;
	
	public JavaHTTPServer(Socket c) {
		connect = c;
	}
	
	public static void main(String[] args) {
		try {
			ServerSocket serverConnect = new ServerSocket(PORT);
			System.out.println("Server started.\nListening for connections on port : " + PORT + " ...\n");
			
			// we listen until user halts server execution
			while (true) {
				JavaHTTPServer myServer = new JavaHTTPServer(serverConnect.accept());
				
				if (verbose) {
					System.out.println("Connecton opened. (" + new Date() + ")");
				}
				
				// create dedicated thread to manage the client connection
				Thread thread = new Thread(myServer);
				thread.start();
			}
			
		} catch (IOException e) {
			System.err.println("Server Connection error : " + e.getMessage());
		}
	}

	@Override
	public void run() {
		// we manage our particular client connection
		BufferedReader in = null; PrintWriter out = null; BufferedOutputStream dataOut = null;
		String fileRequested = null;
		
		try {
			// we read characters from the client via input stream on the socket
			in = new BufferedReader(new InputStreamReader(connect.getInputStream()));
			// we get character output stream to client (for headers)
			out = new PrintWriter(connect.getOutputStream());
			// get binary output stream to client (for requested data)
			dataOut = new BufferedOutputStream(connect.getOutputStream());
			
			// get first line of the request from the client
			String input = in.readLine();
			// we parse the request with a string tokenizer
			StringTokenizer parse = new StringTokenizer(input);
			String method = parse.nextToken().toUpperCase(); // we get the HTTP method of the client
			// we get file requested
			fileRequested = parse.nextToken().toLowerCase();
			
			// we support only GET and HEAD methods, we check
			if (!method.equals("GET")  &&  !method.equals("HEAD")) {
				if (verbose) {
					System.out.println("501 Not Implemented : " + method + " method.");
				}
				
				// we return the not supported file to the client
				File file = new File(WEB_ROOT, METHOD_NOT_SUPPORTED);
				int fileLength = (int) file.length();
				String contentMimeType = "text/html";
				//read content to return to client
				byte[] fileData = readFileData(file, fileLength);
					
				// we send HTTP Headers with data to client
				out.println("HTTP/1.1 501 Not Implemented");
				out.println("Server: Java HTTP Server from SSaurel : 1.0");
				out.println("Date: " + new Date());
				out.println("Content-type: " + contentMimeType);
				out.println("Content-length: " + fileLength);
				out.println(); // blank line between headers and content, very important !
				out.flush(); // flush character output stream buffer
				// file
				dataOut.write(fileData, 0, fileLength);
				dataOut.flush();
				
			} else {
				// GET or HEAD method
				if (fileRequested.endsWith("/")) {
					fileRequested += DEFAULT_FILE;
				}
				
				File file = new File(WEB_ROOT, fileRequested);
				int fileLength = (int) file.length();
				String content = getContentType(fileRequested);
				
				if (method.equals("GET")) { // GET method so we return content
					byte[] fileData = readFileData(file, fileLength);
					
					// send HTTP Headers
					out.println("HTTP/1.1 200 OK");
					out.println("Server: Java HTTP Server from SSaurel : 1.0");
					out.println("Date: " + new Date());
					out.println("Content-type: " + content);
					out.println("Content-length: " + fileLength);
					out.println(); // blank line between headers and content, very important !
					out.flush(); // flush character output stream buffer
					
					dataOut.write(fileData, 0, fileLength);
					dataOut.flush();
				}
				
				if (verbose) {
					System.out.println("File " + fileRequested + " of type " + content + " returned");
				}
				
			}
			
		} catch (FileNotFoundException fnfe) {
			try {
				fileNotFound(out, dataOut, fileRequested);
			} catch (IOException ioe) {
				System.err.println("Error with file not found exception : " + ioe.getMessage());
			}
			
		} catch (IOException ioe) {
			System.err.println("Server error : " + ioe);
		} finally {
			try {
				in.close();
				out.close();
				dataOut.close();
				connect.close(); // we close socket connection
			} catch (Exception e) {
				System.err.println("Error closing stream : " + e.getMessage());
			} 
			
			if (verbose) {
				System.out.println("Connection closed.\n");
			}
		}
		
		
	}
	
	private byte[] readFileData(File file, int fileLength) throws IOException {
		FileInputStream fileIn = null;
		byte[] fileData = new byte[fileLength];
		
		try {
			fileIn = new FileInputStream(file);
			fileIn.read(fileData);
		} finally {
			if (fileIn != null) 
				fileIn.close();
		}
		
		return fileData;
	}
	
	// return supported MIME Types
	private String getContentType(String fileRequested) {
		if (fileRequested.endsWith(".htm")  ||  fileRequested.endsWith(".html"))
			return "text/html";
		else
			return "text/plain";
	}
	
	private void fileNotFound(PrintWriter out, OutputStream dataOut, String fileRequested) throws IOException {
		File file = new File(WEB_ROOT, FILE_NOT_FOUND);
		int fileLength = (int) file.length();
		String content = "text/html";
		byte[] fileData = readFileData(file, fileLength);
		
		out.println("HTTP/1.1 404 File Not Found");
		out.println("Server: Java HTTP Server from SSaurel : 1.0");
		out.println("Date: " + new Date());
		out.println("Content-type: " + content);
		out.println("Content-length: " + fileLength);
		out.println(); // blank line between headers and content, very important !
		out.flush(); // flush character output stream buffer
		
		dataOut.write(fileData, 0, fileLength);
		dataOut.flush();
		
		if (verbose) {
			System.out.println("File " + fileRequested + " not found");
		}
	}
	
}

参考链接


Create a simple HTTP Web Server in Java

Android Studio 4.0.1 报错:Entry name 'AndroidManifest.xml' collided

最近在集成极光推送的时候,需要集成极光提供的魅族推送SDK, 其实也就是把魅族官方的推送SDK进行了简单的封装。

但是带编译的时候报告如下错误:

Entry name 'AndroidManifest.xml' collided

反复检查许久,不得要领。

经过解压提供的 AAR 包后,继续解压缩里面的 classes.jar ,发现 JAR 包里面包含 AndroidManifest.xml 。

导致在 Android Gradle plugin 3.6之后的版本编译出现异常。

如果想彻底解决这个问题,需要移除 JAR 包中的 AndroidManifest.xml

参考链接


Android Studio 4.0.1编译报错Error:Execution failed for task ':app:transformClassesWithProfilers-transformForXXXXDebug'.

Android Studio 4.0.1 编译调试应用的时候报错:

Error:Execution failed for task ':app:transformClassesWithProfilers-transformForXXXXDebug'.

但是当被调试的手机是Android 10系统的时候,不会出现报错。当插入的手机是 Android 6.1系统的时候,报告上面的错误信息。

在命令行下执行

$ bash gradlew clean build

却一直都是成功的。

百思不解。

经过研究发现,是在运行调试配置信息界面中开启了 "Enable advanced profiling" 功能的时候,才会出现上面的情况。

关闭这个功能就正常了。

具体配置参考下图:

继续阅读Android Studio 4.0.1编译报错Error:Execution failed for task ':app:transformClassesWithProfilers-transformForXXXXDebug'.

macOS Catalina配置Android反编译三件套 apktool/dex2jar/enjarify/jd-gui

创建目录

$ cd ~

$ mkdir Android

$ cd Android

下载安装配置dex2jar

$ wget https://github.com/pxb1988/dex2jar/files/1867564/dex-tools-2.1-SNAPSHOT.zip

// 本站 wget https://www.mobibrw.com/wp-content/uploads/2020/07/dex-tools-2.1-SNAPSHOT.zip

$ unzip dex-tools-2.1-SNAPSHOT.zip

$ ln -s dex-tools-2.1-SNAPSHOT dex2jar

$ cd dex2jar

$ chmod +x *.sh

$ ln -s d2j-dex2jar.sh dex2jar

$ echo 'export PATH=$PATH:~/Android/dex2jar' >> ~/.bashrc

$ source ~/.bashrc

# 直接从APK转换,2.1版本开始支持multidex
$ dex2jar xxx.apk

Enjarify 是一个将Dalvik字节码转化为等价的Java字节码,然后可以用一系列的Java分析工具去分析Android应用。之前我们一直使用Dex2jar来完成这个工作,不过Dex2jar已经算是一个非常老的工具咯。Dex2jar在绝大部分情况下都挺不错的,但是如果碰到了些模糊的特性或者特殊案例就会出错或者默默地吐出一些错误的结果。相比之下,Enjarify在设计的时候就考虑到了尽可能多的情况,特别是对于那些Dex2jar不起作用的情况下。另外,Enjarify能够有效地处理Unicode编码的类名、常量,隐式类型转换、正常处理流程中的移除处理等等。

下载安装配置Enjarify

$ cd ~/Android

$ git clone https://github.com/Storyyeller/enjarify.git

//本站下载 wget https://www.mobibrw.com/wp-content/uploads/2020/07/enjarify.zip

$ cd enjarify

# 只能在~/Android/enjarify目录下执行
$ enjarify yourapp.apk

下载安装配置apktool

$ cd ~/Android

$ mkdir apktool

$ cd apktool

$ wget https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.4.1.jar

// 本站下载 wget https://www.mobibrw.com/wp-content/uploads/2020/07/apktool_2.4.1.jar_.zip

$ ln -s apktool_2.4.1.jar apktool.jar

$ echo 'export PATH=$PATH:~/Android/apktool' >> ~/.bashrc

$ echo "alias apktool='java -jar ~/Android/apktool/apktool.jar'" >> ~/.bashrc

$ source ~/.bashrc

# 提取资源文件相关
$ apktool d xxx.apk

下载安装配置jd-gui

$ cd ~/Android 

$ wget https://github.com/java-decompiler/jd-gui/releases/download/v1.6.6/jd-gui-osx-1.6.6.tar

# 本站下载 wget https://www.mobibrw.com/wp-content/uploads/2020/07/jd-gui-osx-1.6.6.tar

$ tar xvf jd-gui-osx-1.6.6.tar

参考链接


30分钟掌握Dart语言

  • 在Dart中,一切都是对象,一切对象都是class的实例,哪怕是数字类型、方法甚至null都是对象,所有的对象都是继承自Object
  • 虽然Dart是强类型语言,但变量类型是可选的因为Dart可以自动推断变量类型
  • Dart支持范型,List<int>表示一个整型的数据列表,List<dynamic>则是一个对象的列表,其中可以装任意对象
  • Dart支持顶层方法(如main方法),也支持类方法或对象方法,同时你也可以在方法内部创建方法
  • Dart支持顶层变量,也支持类变量或对象变量
  • 跟Java不同的是,Dart没有public protected private等关键字,如果某个变量以下划线(_)开头,代表这个变量在库中是私有的,具体可以看这里
  • Dart中变量可以以字母或下划线开头,后面跟着任意组合的字符或数字

变量

变量定义

以下代码是Dart中定义变量的方法:

main() {
  var a = 1;
  int b = 10;
  String s = "hello";
  dynamic c = 0.5;
}

你可以明确指定某个变量的类型,如int bool String,也可以用var或 dynamic来声明一个变量,Dart会自动推断其数据类型。

变量的默认值

注意:没有赋初值的变量都会有默认值null

final和const

如果你绝不想改变一个变量,使用final或const,不要使用var或其他类型,一个被final修饰的变量只能被赋值一次,一个被const修饰的变量是一个编译时常量(const常量毫无疑问也是final常量)。可以这么理解:final修饰的变量是不可改变的,而const修饰的表示一个常量。

注意:实例变量可以是final的但不能是const的

下面用代码说明:

var count = 10; 
final Num = count;  // final 只能赋值一次
const Num1 = 10; // const赋值必须是编译时常量

final和const的区别:

区别一:final 要求变量只能初始化一次,并不要求赋的值一定是编译时常量,可以是常量也可以不是。而 const 要求在声明时初始化,并且赋值必需为编译时常量。

区别二:final 是惰性初始化,即在运行时第一次使用前才初始化。而 const 是在编译时就确定值了。

内建数据类型

Dart有如下几种内建的数据类型:

  • numbers
  • strings
  • booleans
  • lists(或者是arrays)
  • maps
  • runes(UTF-32字符集的字符)
  • symbols
    下面用一段代码来演示以上各类数据类型:
main() {
  // numbers
  var a = 0;
  int b = 1;
  double c = 0.1;

  // strings
  var s1 = 'hello';
  String s2 = "world";

  // booleans
  var real = true;
  bool isReal = false;

  // lists
  var arr = [1, 2, 3, 4, 5];
  List<String> arr2 = ['hello', 'world', "123", "456"];
  List<dynamic> arr3 = [1, true, 'haha', 1.0];

  // maps
  var map = new Map();
  map['name'] = 'zhangsan';
  map['age'] = 10;
  Map m = new Map();
  m['a'] = 'a';

  //runes,Dart 中 使用runes 来获取UTF-32字符集的字符。String的 codeUnitAt and codeUnit属性可以获取UTF-16字符集的字符
  var clapping = '\u{1f44f}';
  print(clapping); // 打印的是拍手emoji的表情

  // symbols
  print(#s == new Symbol("s")); // true
}

函数

函数的返回值

Dart是一个面向对象的编程语言,所以即使是函数也是一个对象,也有一种类型Function,这就意味着函数可以赋值给某个变量或者作为参数传给另外的函数。虽然Dart推荐你给函数加上返回值,但是不加返回值的函数同样可以正常工作,另外你还可以用=>代替return语句,比如下面的代码:

// 声明返回值
int add(int a, int b) {
  return a + b;
}
// 不声明返回值
add2(int a, int b) {
  return a + b;
}
// =>是return语句的简写
add3(a, b) => a + b; 

main() {
  print(add(1, 2)); // 3
  print(add2(2, 3)); // 5
  print(add3(1, 2)); // 3
}

命名参数、位置参数、参数默认值

命名参数

sayHello({String name}) {
  print("hello, my name is $name");
}
sayHello2({name: String}) {
  print("hello, my name is $name");
}
main() {
  // 打印 hello, my name is zhangsan
  sayHello(name: 'zhangsan');

  // 打印 hello, my name is wangwu
  sayHello2(name: 'wangwu');
}

可以看到,定义命名参数时,你可以以 {type paramName} 或者 {paramName: type} 两种方式声明参数,而调用命名参数时,需要以 funcName(paramName: paramValue) 的形式调用。

命名参数的参数并不是必须的,所以上面的代码中,如果调用sayHello()不带任何参数,也是可以的,只不过最后打印出来的结果是:hello, my name is null,在Flutter开发中,你可以使用@required注解来标识一个命名参数,这代表该参数是必须的,你不传则会报错,比如下面的代码:

const Scrollbar({Key key, @required Widget child})

位置参数

使用中括号[]括起来的参数是函数的位置参数,代表该参数可传可不传,位置参数只能放在函数的参数列表的最后面,如下代码所示:

sayHello(String name, int age, [String hobby]) { // 位置参数可以有多个,比如[String a, int b]
  StringBuffer sb = new StringBuffer();
  sb.write("hello, this is $name and I am $age years old");
  if (hobby != null) {
    sb.write(", my hobby is $hobby");
  }
  print(sb.toString());
}
main() {
  // hello, this is zhangsan and I am 20 years old
  sayHello("zhangsan", 20);
  // hello, this is zhangsan and I am 20 years old, my hobby is play football
  sayHello("zhangsan", 20, "play football");
}

参数默认值

你可以为命名参数或者位置参数设置默认值,如下代码所示:

// 命名参数的默认值
int add({int a, int b = 3}) { // 不能写成:int add({a: int, b: int = 3})
  return a + b;
}
// 位置参数的默认值
int sum(int a, int b, [int c = 3]) {
  return a + b + c;
}

main()函数

不论在Dart还是Flutter中,必须都需要一个顶层的main()函数,它是整个应用的入口函数,main()函数的返回值是void,还有一个可选的参数,参数类型是List<String>。

函数作为一类对象

你可以将一个函数作为参数传给另一个函数,比如下面的代码:

printNum(int a) {
  print("$a");
}
main() {
  //  依次打印:
  //  1
  //  2
  //  3
  var arr = [1, 2, 3];
  arr.forEach(printNum);
}

你也可以将一个函数赋值给某个变量,比如下面的代码:

printNum(int a) {
  print("$a");
}
main() {
  var f1 = printNum;
  Function f2 = printNum;
  var f3 = (int a) => print("a = $a");
  f1(1);
  f2(2);
  f3(6);
}

匿名函数

大多数函数都是有名称的,比如main() printName()等,但是你也可以写匿名函数,如果你对Java比较熟悉,那下面的Dart代码你肯定也不会陌生:

test(Function callback) {
  callback("hello");
}
main() {
  test((param) {
    // 打印hello
    print(param);
  });
}

匿名函数类似于Java中的接口,往往在某个函数的参数为函数时使用到。

函数返回值

所有的函数都有返回值,如果没有指定return语句,那么该函数的返回值为null。

运算符

Dart中的运算符与Java中的类似,比如++a a == b b ? a : b,但是也有一些与Java不太一样的运算符,下面用代码说明:

main() {
  // 与Java相同的运算符操作

  int a = 1;
  ++a;
  a++;
  var b = 1;
  print(a == b);  // false
  print(a * b); // 3
  bool real = false;
  real ? print('real') : print('not real'); // not real
  print(real && a == b); // false
  print(real || a == 3); // true
  print(a != 2); // true
  print(a <= b); // false
  var c = 9;
  c += 10;
  print("c = $c"); // c = 19
  print(1<<2); // 4

  // 与Java不太一样的运算符操作

  // is运算符用于判断一个变量是不是某个类型的数据
  // is!则是判断变量不是某个类型的数据
  var s = "hello";
  print(s is String); // true
  var num = 6;
  print(num is! String); // true

  // ~/才是取整运算符,如果使用/则是除法运算,不取整
  int k = 1;
  int j = 2;
  print(k / j); // 0.5
  print(k ~/ j); // 0

  // as运算符类似于Java中的cast操作,将一个对象强制类型转换
  (emp as Person).teach();

  // ??=运算符 如果 ??= 运算符前面的变量为null,则赋值,否则不赋值
  var param1 = "hello", param2 = null;
  param1 ??= "world";
  param2 ??= "world";
  print("param1 = $param1"); // param1 = hello
  print("param2 = $param2"); // param2 = world
  
  // ?.运算符
  var str1 = "hello world";
  var str2 = null;
  print(str1?.length); // 11
  print(str2?.length); // null 
  print(str2.length); // 报错
}

..运算符(级联操作)

如果你对Java中的建造者模式比较熟悉的话,Dart中的..运算符也很好理解,先看下面的代码:

class Person {
  eat() {
    print("I am eating...");
  }

  sleep() {
    print("I am sleeping...");
  }

  study() {
    print("I am studying...");
  }
}
main() {
  // 依次打印
  //  I am eating...
  //  I am sleeping...
  //  I am studying...
  new Person()..eat()
      ..sleep()
      ..study();
}

可以看到,使用..调用某个对象的方法(或者成员变量)时,返回值是这个对象本身,所以你可以接着使用..调用这个对象的其他方法,这不就类似于Java中的建造者模式,每次build某个属性时,都返回一个this对象吗。

控制流程

if / else switch for /while try / catch语句跟Java中都类似,try / catch语句可能稍有不同,下面用一段代码说明:

main() {
  // if else语句
  int score = 80;
  if (score < 60) {
    print("so bad!");
  } else if (score >= 60 && score < 80) {
    print("just so so!");
  } else if (score >= 80) {
    print("good job!");
  }

  // switch语句
  String a = "hello";
  // case语句中的数据类型必须是跟switch中的类型一致
  switch (a) {
    case "hello":
      print("haha");
      break;
    case "world":
      print("heihei");
      break;
    default:
      print("WTF");
  }

  // for语句
  List<String> list = ["a", "b", "c"];
  for (int i = 0; i < list.length; i++) {
    print(list[i]);
  }
  for (var i in list) {
    print(i);
  }
  // 这里的箭头函数参数必须用圆括号扩起来
  list.forEach((item) => print(item));

  // while语句
  int start = 1;
  int sum = 0;
  while (start <= 100) {
    sum += start;
    start++;
  }
  print(sum);

  // try catch语句
  try {
    print(1 ~/ 0);
  } catch (e) {
    // IntegerDivisionByZeroException
    print(e);
  }
  try {
    1 ~/ 0;
  } on IntegerDivisionByZeroException { // 捕获指定类型的异常
    print("error"); // 打印出error
  } finally {
    print("over"); // 打印出over
  }
}

类(Class)

类的定义与构造方法

Dart中的类没有访问控制,所以你不需要用private, protected, public等修饰成员变量或成员函数,一个简单的类如下代码所示:

class Person {
  String name;
  int age;
  String gender;
  Person(this.name, this.age, this.gender);
  sayHello() {
    print("hello, this is $name, I am $age years old, I am a $gender");
  }
}

上面的Person类中有3个成员变量,一个构造方法和一个成员方法,看起来比较奇怪的是Person的构造方法,里面传入的3个参数都是this.xxx,而且没有大括号{}包裹的方法体,这种语法是Dart比较独特而简洁的构造方法声明方式,它等同于下面的代码:

Person(String name, int age, String gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
}

要调用Person类的成员变量或成员方法,可以用下面的代码:

  var p = new Person("zhangsan", 20, "male");
  p.sayHello(); // hello, this is zhangsan, I am 20 years old, I am a male
  p.age = 50;
  p.gender = "female";
  p.sayHello(); // hello, this is zhangsan, I am 50 years old, I am a female

类除了有跟类名相同的构造方法外,还可以添加命名的构造方法,如下代码所示:

class Point {
  num x, y;
  Point(this.x, this.y);
  // 类的命名构造方法
  Point.origin() {
    x = 0;
    y = 0;
  }
}
main() {
  // 调用Point类的命名构造方法origin()
  var p = new Point.origin();
  var p2 = new Point(1, 2);
}

Dart中使用extends关键字做类的继承,如果一个类只有命名的构造方法,在继承时需要注意,如下代码:

class Human {
  String name;
  Human.fromJson(Map data) {
    print("Human's fromJson constructor");
  }
}
class Man extends Human {
  Man.fromJson(Map data) : super.fromJson(data) {
    print("Man's fromJson constructor");
  }
}

由于Human类没有默认构造方法,只有一个命名构造方法fromJson,所以在Man类继承Human类时,需要调用父类的fromJson方法做初始化,而且必须使用Man.fromJson(Map data) : super.fromJson(data)这种写法,而不是像Java那样将super写到花括号中。

有时候你仅仅只是在某个类的构造方法中,调用这个类的另一个构造方法,你可以这么写:

class Point {
  num x, y;
  Point(this.x, this.y);
  // 命名构造方法调用了默认的构造方法
  Point.alongXAxis(num x) : this(x, 0);
}

类的成员方法

一个类的成员方法是一个函数,为这个类提供某些行为。上面的代码中已经有了一些类的成员方法的定义,这些定义方式跟Java很类似,你可以为某个类的成员变量提供getter/setter方法,如下代码:

class Rectangle {
  num left, top, width, height;

  // 构造方法传入left, top, width, height几个参数
  Rectangle(this.left, this.top, this.width, this.height);

  // right, bottom两个成员变量提供getter/setter方法
  num get right => left + width;
  set right(num value) => left = value - width;
  num get bottom => top + height;
  set bottom(num value) => top = value - height;
}

抽象类和抽象方法

使用abstract修饰一个类,则这个类是抽象类,抽象类中可以有抽象方法和非抽象方法,抽象方法没有方法体,需要子类去实现,如下代码:

abstract class Doer {
  // 抽象方法,没有方法体,需要子类去实现
  void doSomething();
  // 普通的方法
  void greet() {
    print("hello world!");
  }
}
class EffectiveDoer extends Doer {
  // 实现了父类的抽象方法
  void doSomething() {
    print("I'm doing something...");
  }
}

运算符重载

Dart中有类似于C++中的运算符重载语法,比如下面的代码定义了一个向量类,重载了向量的+ -运算:

class Vector {
  num x, y;
  Vector(this.x, this.y);
  Vector operator +(Vector v) => new Vector(x + v.x, y + v.y);
  Vector operator -(Vector v) => new Vector(x - v.x, y - v.y);
  printVec() {
    print("x: $x, y: $y");
  }
}
main() {
  Vector v1 = new Vector(1, 2);
  Vector v2 = new Vector(3, 4);
  (v1 - v2).printVec(); // -2, -2
  (v1 + v2).printVec(); // 4, 6
}

枚举类

使用enum关键字定义一个枚举类,这个语法跟Java类似,如下代码:

enum Color { red, green, blue }

mixins

mixins是一个重复使用类中代码的方式,比如下面的代码:

class A {
  a() {
    print("A's a()");
  }
}
class B {
  b() {
    print("B's b()");
  }
}
// 使用with关键字,表示类C是由类A和类B混合而构成
class C = A with B;
main() {
  C c = new C();
  c.a(); // A's a()
  c.b(); // B's b()
}

静态成员变量和静态成员方法

// 类的静态成员变量和静态成员方法
class Cons {
  static const name = "zhangsan";
  static sayHello() {
    print("hello, this is ${Cons.name}");
  }
}
main() {
  Cons.sayHello(); // hello, this is zhangsan
  print(Cons.name); // zhangsan
}

泛型(Generics)

Java和C++语言都有泛型,Dart语言也不例外,使用泛型有很多好处,比如:

正确指定泛型类型会产生更好的生成代码。
泛型可以减小代码的复杂度

Dart内置的数据类型List就是一个泛型数据类型,你可以往List中塞任何你想的数据类型比如整型、字符串、布尔值等
关于Dart更多的泛型知识点,可以查看这里。

Dart库(Libraries)

Dart目前已经有很多的库提供给开发者,许多功能不需要开发者自己去实现,只需要导入对应的包即可,使用import语句来导入某个包,比如下面的代码:

import 'dart:html';

如果你想导入自己写的某个代码文件,使用相对路径即可,例如当前有一个demo.dart文件,跟该文件同级目录下有个util.dart文件,文件代码如下:

// util.dart文件内容
int add(int a, int b) {
  return a + b;
}

在demo.dart文件中如果要引用util.dart文件,使用下面的方式导入:

// demo.dart
import './util.dart';
main() {
  print(add(1, 2));
}

你可以使用as关键字为导入的某个包设置一个前缀,或者说别名,比如下面的代码:

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// Uses Element from lib1.
Element element1 = Element();
// Uses Element from lib2.
lib2.Element element2 = lib2.Element();

你也可以在导入包时使用show hide关键字来导入某个包中的部分功能,比如下面的代码:

// 只导入foo
import 'package:lib1/lib1.dart' show foo;
// 导入除了foo的所有其他部分
import 'package:lib2/lib2.dart' hide foo;

导入包时使用deferred as可以让这个包懒加载,懒加载的包只会在该包被使用时得到加载,而不是一开始就加载,比如下面的代码:

import 'package:greetings/hello.dart' deferred as hello;

异步

Dart提供了类似ES7中的async await等异步操作,这种异步操作在Flutter开发中会经常遇到,比如网络或其他IO操作,文件选择等都需要用到异步的知识。
async和await往往是成对出现的,如果一个方法中有耗时的操作,你需要将这个方法设置成async,并给其中的耗时操作加上await关键字,如果这个方法有返回值,你需要将返回值塞到Future中并返回,如下代码所示:

Future checkVersion() async {
  var version = await lookUpVersion();
  // Do something with version
}

下面的代码使用Dart从网络获取数据并打印出来:

import 'dart:async';
import 'package:http/http.dart' as http;

Future<String> getNetData() async{
  http.Response res = await http.get("http://www.baidu.com");
  return res.body;
}
main() {
  getNetData().then((str) {
    print(str);
  });
}

参考链接


30分钟掌握Dart语言