`
azheng270
  • 浏览: 93295 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

ANT安装配置笔记和最佳实践

阅读更多



ANT的安装/配置笔记
作者:车东发表于:2003-03-0617:03最后更新于:2007-04-1211:04
版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明。
http://www.chedong.com/tech/ant.html
--------------------------------------------------------------------------------
内容摘要:
ant是一个基于JAVA的自动化脚本引擎,脚本格式为XML。除了做JAVA编译相关任务外,ANT还可以通过插件实现很多应用的调用。


ANT的基本概念:
ANT的安装:解包,设置路径
ANT的使用:最好的学习只不过是一个简单实用的例子起步……
ANT的基本概念:Java的Makefile
当一个代码项目大了以后,每次重新编译,打包,测试等都会变得非常复杂而且重复,因此c语言中有make脚本来帮助这些工作的批量完成。在Java中应用是平台无关性的,当然不会用平台相关的make脚本来完成这些批处理任务了,ANT本身就是这样一个流程脚本引擎,用于自动化调用程序完成项目的编译,打包,测试等。除了基于JAVA是平台无关的外,脚本的格式是基于XML的,比make脚本来说还要好维护一些。


每个ant脚本(缺省叫build.xml)中设置了一系列任务(target):比如对于一个一般的项目可能需要有以下任务。

任务1:usage打印本脚本的帮助信息(缺省)
任务2:clean
<--init清空初始化环境
任务3:javadoc<--build<--init生成JAVADOC
任务4:jar<--build<--init生成JAR
任务5:all<--jar+javadoc<--build<--init完成以上所有任务:jarjavadoc
而多个任务之间往往又包含了一定了依赖关系:比如把整个应用打包任务(jar)的这个依赖于编译任务(build),而编译任务又依赖于整个环境初始化任务(init)等。

注:我看到很多项目的ant脚本中的命名基本上都是一致的,比如:编译一般叫build或者compile;打包一般叫jar或war;生成文档一般命名为javadoc或javadocs;执行全部任务all。在每个任务的中,ANT会根据配置调用一些外部应用并配以相应参数执行。虽然ANT可调用的外部应用种类非常丰富,但其实最常用的就2,3个:比如javacjavadocjar等。
ANT的安装
解包后在系统可执行路径中加入指向ant的bin的路径就可以了,比如可以在GNU/Linux上把以下配置加入/etc/profile中:
exportANT_HOME
=/home/ant
exportJAVA_HOME=/usr/java/j2sdk1.4.1
exportPATH=$PATH:$JAVA_HOME/bin:$ANT_HOME/bin

这样执行ant后,如果不指定配置文件ant会缺省找build.xml这个配置文件,并根据配置文件执行任务,缺省的任务设置可以指向最常用的任务,比如:build,或指向打印帮助信息:usage,告诉用户有那些脚本选项可以使用。


ANT的使用

最好的学习过程就是看懂那些opensource项目中的build.xml脚本,然后根据自己的需要简化成一个更简单的,ANT和APACHE上很多非常工程派的项目:简单易用,而且适应性非常强,因为这些项目的建立往往来源于开发人员日常最直接的需求。
以下是的一个WebLucene应用的例子:修改自JDOM的build.xml:

<projectdefault
="usage"basedir=".">

<!--===================================================================-->
<!--Initializationtarget-->
<!--===================================================================-->
<targetname="init">
<tstamp/>
<propertyfile="${basedir}/build.properties"/>
<propertyname="Name"value="ProjectFullName"/>
<propertyname="name"value="project_name"/>
<propertyname="version"value="0.2"/>
<propertyname="year"value="2003"/>

<echomessage="-----------${Name}${version}[${year}]------------"/>

<propertyname="debug"value="off"/>
<propertyname="optimize"value="on"/>
<propertyname="deprecation"value="on"/>

<propertyname="src.dir"value="./src/WEB-INF/src"/>
<propertyname="lib.dir"value="./src/WEB-INF/lib"/>
<propertyname="packages"value="com.chedong.*,org.apache.lucene.*"/>

<propertyname="build.src"value="./src/WEB-INF/build"/>
<propertyname="build.dest"value="./src/WEB-INF/classes"/>
<propertyname="build.javadocs"value="./src/doc"/>

<pathid="classpath">
<pathelementpath="${jsdk_jar}"/>
<filesetdir="${lib.dir}">
<includename="**/*.jar"/>
</fileset>
</path>

<filtertoken="year"value="${year}"/>
<filtertoken="version"value="${version}"/>
<filtertoken="date"value="${TODAY}"/>
<filtertoken="log"value="true"/>
<filtertoken="verbose"value="true"/>
</target>

<!--===================================================================-->
<!--Helponusage-->
<!--===================================================================-->
<targetname="usage"depends="init">
<echomessage="${Name}Buildfile"/>
<echomessage="-------------------------------------------------------------"/>
<echomessage=""/>
<echomessage="availabletargetsare:"/>
<echomessage=""/>
<echomessage="jar-->generatesthe${name}.jarfile"/>
<echomessage="build-->compilesthesourcecode"/>
<echomessage="javadoc-->generatestheAPIdocumentation"/>
<echomessage="clean-->cleansupthedirectory"/>
<echomessage=""/>
<echomessage="Pleaserenamebuild.properties.defaulttobuild.properties"/>
<echomessage="andeditbuild.propertiestospecifyJSDK2.3classpath."/>
<echomessage=""/>
<echomessage="Seethecommentsinsidethebuild.xmlfileformoredetails."/>
<echomessage="-------------------------------------------------------------"/>
<echomessage=""/>
<echomessage=""/>
</target>

<!--===================================================================-->
<!--Preparesthesourcecode-->
<!--===================================================================-->
<targetname="prepare-src"depends="init">
<!--createdirectories-->
<mkdirdir="${build.src}"/>
<mkdirdir="${build.dest}"/>

<!--copysrcfiles-->
<copytodir="${build.src}">
<filesetdir="${src.dir}"/>
</copy>
</target>

<!--===================================================================-->
<!--Compilesthesourcedirectory-->
<!--===================================================================-->
<targetname="build"depends="prepare-src">
<javacsrcdir="${build.src}"
destdir
="${build.dest}"
debug
="${debug}"
optimize
="${optimize}">
<classpathrefid="classpath"/>
</javac>
</target>

<!--===================================================================-->
<!--Createstheclasspackage-->
<!--===================================================================-->
<targetname="jar"depends="build">
<jarjarfile="${lib.dir}/${name}.jar"
basedir
="${build.dest}"
includes
="**"/>
</target>

<!--===================================================================-->
<!--CreatestheAPIdocumentation-->
<!--===================================================================-->
<targetname="javadoc"depends="build">
<mkdirdir="${build.javadocs}"/>
<javadocpackagenames="${packages}"
sourcepath
="${build.src}"
destdir
="${build.javadocs}"
author
="true"
version
="true"
use
="true"
splitindex
="true"
windowtitle
="${Name}API"
doctitle
="${Name}">
<classpathrefid="classpath"/>
</javadoc>
</target>

<!--===================================================================-->
<!--Cleantargets-->
<!--===================================================================-->
<targetname="clean"depends="init">
<deletedir="${build.src}"/>
<deletedir="${build.dest}/org"/>
<deletedir="${build.dest}/com"/>
<delete>
<filesetdir="${build.dest}"includes="**/*.class"/>
</delete>
</target>
</project>
<!--Endoffile-->

缺省任务:usage打印帮助文档,告诉有那些任务选项:可用的有build,jar,javadoc和clean.

初始化环境变量:init
所有任务都基于一些基本环境变量的设置初始化完成,是后续其他任务的基础,在环境初始化过程中,有2点比较可以方便设置:

1除了使用却缺省的property设置了JAVA源路径和输出路径外,引用了一个外部的build.properties文件中的设置,
<propertyfile="${basedir}/build.properties"/>
这样大部分简单配置用户只要会看懂build.properties就可以了,毕竟XML比起keyvalue的属性文件还是要可读性差一些。用build.properties也可以方便其他用户从编译的细节中解放出来。

2CLASSPATH设置:使用了其中的:
<pathid="classpath">
<pathelementpath="${jsdk_jar}"/>
<filesetdir="${lib.dir}">
<includename="**/*.jar"/>
</fileset>
</path>
则相当于设置了:CLASSPATH=/path/to/resin/lib/jsdk23.jar;/path/to/project/lib/*.jar;

文件复制:prepare-src
创建临时SRC存放目录和输出目录。
<!--===================================================================-->
<!--Preparesthesourcecode-->
<!--===================================================================-->
<targetname="prepare-src"depends="init">
<!--createdirectories-->
<mkdirdir="${build.src}"/>
<mkdirdir="${build.dest}"/>

<!--copysrcfiles-->
<copytodir="${build.src}">
<filesetdir="${src.dir}"/>
</copy>
</target>

编译任务:build
编译时的CLASSPATH环境通过一下方式找到引用一个path对象
<classpathrefid="classpath"/>

打包任务:jar
对应用打包生成项目所写名的.jar文件
<!--===================================================================-->
<!--Createstheclasspackage-->
<!--===================================================================-->
<targetname="jar"depends="build">
<jarjarfile="${lib.dir}/${name}.jar"
basedir
="${build.dest}"
includes
="**"/>
</target>

生成JAVADOC文档任务:javadoc
<!--===================================================================-->
<!--CreatestheAPIdocumentation-->
<!--===================================================================-->
<targetname="javadoc"depends="build">
<mkdirdir="${build.javadocs}"/>
<javadocpackagenames="${packages}"
sourcepath
="${build.src}"
destdir
="${build.javadocs}"
author
="true"
version
="true"
use
="true"
splitindex
="true"
windowtitle
="${Name}API"
doctitle
="${Name}">
<classpathrefid="classpath"/>
</javadoc>
</target>

清空临时编译文件:clean
<!--===================================================================-->
<!--Cleantargets-->
<!--===================================================================-->
<targetname="clean"depends="init">
<deletedir="${build.src}"/>
<deletedir="${build.dest}/org"/>
<deletedir="${build.dest}/com"/>
<delete>
<filesetdir="${build.dest}"includes="**/*.class"/>
</delete>
</target>

TODO:
更多任务/扩展:(样例)

测试任务:JUnit测试
代码风格检查任务:CheckStyle,Jalopy等
邮件警报任务:可以把以上这些任务的输出警告发送到制定的用户列表中,这个任务可以设置每天自动运行。

参考资料:

JakartaANT:
http://ant.apache.org


ANT十五大最佳实践
作者:EricM.Burke,coauthorofJavaExtremeProgrammingCookbook

原文:http://www.onjava.com/pub/a/onjava/2003/12/17/ant_bestpractices.html

译者:徐彤MSN:xt121@hotmail.com



在Ant出现之前,构建和部署Java应用需要使用包括特定平台的脚本、Make文件、各种版本的IDE甚至手工操作的“大杂烩”。现在,几乎所有的开源Java项目都在使用Ant,大多数公司的内部项目也在使用Ant。Ant在这些项目中的广泛使用自然导致了读者对一整套Ant最佳实践的迫切需求。

本文总结了我喜爱的Ant技巧或最佳实践,多数是从我亲身经历的项目错误或我听说的其他人经历的“恐怖”故事中得到灵感的。比如,有人告诉我有个项目把XDoclet生成的代码放入带有锁定文件功能的版本控制工具中。当开发者修改源代码时,他必须记住手工检出(Checkout)并锁定所有将要重新生成的文件。然后,手工运行代码生成器,只到这时他才能够让Ant编译代码,这一方法还存在如下一些问题:

生成的代码无法存储在版本控制系统中。
Ant(本案例中是Xdoclet)应该自动确定下一次构建涉及的源文件,而不应由程序员手工确定。
Ant的构建文件应该定义好正确的任务依赖关系,这样程序员就不必为了完成构建而不得不按照特定顺序调用任务。
当我开始一个新项目时,我首先编写Ant构建文件。Ant文件明确地定义构建的过程,并被团队中的每个程序员使用。本文所列的技巧基于这样的假定:Ant构建文件是一个必须仔细编写的重要文件,它应在版本控制系统中得到维护,并被定期进行重构。下面是我的十五大Ant最佳实践。

1.采用一致的编码规范
Ant用户有的喜欢有的痛恨其构建文件的XML语法。与其跳进这一令人迷惑的争论中,不如让我们先看一些能保持XML构建文件简洁的方法。

首先也是最重要的,花费时间格式化你的XML让它看上去很清晰。不论XML是否美观,Ant都可以工作。但是丑陋的XML很难令人读懂。倘若你在任务之间留出空行,有规则的缩进,每行文字不超过90列左右,那么XML令人惊讶地易读。再加上使用能够高亮XML语法的优秀编辑器或IDE工具,你就不会有阅读的麻烦。

同样,精选含意明确、容易读懂的词汇来命名任务和属性。比如,dir.reports就比rpts好。特定的编码规范并不重要,只要拿出一套规范并坚持使用就行。

2.将build.xml放在项目根目录中
Ant构建文件build.xml可以放在任何位置,但是放在项目顶级目录中可以保持项目简洁。这是最常用的规范,开发者能够在顶级目录中找到预期的build.xml。把构建文件放在根目录中,也能够使人容易了解项目目录树中不同目录之间的逻辑关系。以下是一个典型的项目目录层次:

[rootdir]
|build.xml
+--src
+--lib(包含第三方JAR包)
+--build(由build任务生成)
+--dist(由build任务生成)

当build.xml在顶级目录时,假设你处于项目某个子目录中,只要输入:ant-findcompile命令,不需要改变工作目录就能够以命令行方式编译代码。参数-find告诉Ant寻找存在于上级目录中的build.xml并执行。

3.使用单一的构建文件
有人喜欢将一个大项目分解成几个小的构建文件,每个构建文件分担整个构建过程的一小部分工作。这确实是看法不同的问题,但是应该认识到,将构建文件分割会增加对整体构建过程的理解难度。要注意在单一构建文件能够清楚表现构建层次的情况下不要过工程化(over-engineer)。

即使你把项目划分为多个构建文件,也应使程序员能够在项目根目录下找到核心build.xml。尽管该文件只是将实际构建工作委派给下级构建文件,也应保证该文件可用。

4.提供良好的帮助说明
应尽量使构建文件自文档化。增加任务描述是最简单的方法。当你输入ant-projecthelp时,你就可以看到带有描述的任务清单。比如,你可以这样定义任务:

<targetname="compile"
description
="Compilescode,outputgoestothebuilddir.">
最简单的规则是把所有你想让程序员通过命令行就可以调用的任务都加上描述。对于一般用来执行中间处理过程的内部任务,比如生成代码或建立输出目录等,就无法使用描述属性。

这时,可以通过在构建文件中加入XML注释来处理。或者专门定义一个help任务,当程序员输入anthelp时来显示详细的使用说明。

<targetname="help"description="Displaydetailedusageinformation">
<echo>Detailedhelp...</echo></target>
5.提供清除任务
每个构建文件都应包含一个清除任务,用来删除所有生成的文件和目录,使系统回到构建文件执行前的初始状态。执行清空任务后还存在的文件都应处在版本控制系统的管理之下。比如:

<targetname="clean"
description
="Destroysallgeneratedfilesanddirs.">
<deletedir="${dir.build}"/>
<deletedir="${dir.dist}"/>
</target>
除非是在产生整个系统版本的特殊任务中,否则不要自动调用clean任务。当程序员仅仅执行编译任务或其他任务时,他们不需要构建文件事先执行既令人讨厌又没有必要的清空任务。要相信程序员能够确定何时需要清空所有文件。

6.使用ANT管理任务从属关系
假设你的应用由SwingGUI组件、Web界面、EJB层和公共应用代码组成。在大型系统中,你需要清晰地定义每个Java包属于系统的哪一层。否则任何一点修改都要被迫重新编译成百上千个文件。糟糕的任务从属关系管理会导致过度复杂而脆弱的系统。改变GUI面板的设计不应造成Servlet和EJB的重编译。

当系统变得庞大后,稍不注意就可能将依赖于客户端的代码引入到服务端。这是因为典型的IDE项目文件编译任何文件都使用单一的classpath。而Ant能让你更有效地控制构建活动。

设计你的Ant构建文件编译大型项目的步骤:首先,编译公共应用代码,将编译结果打成JAR包文件。然后,编译上一层的项目代码,编译时依靠第一步产生的JAR文件。不断重复这一过程,直到最高层的代码编译完成。

分步构建强化了任务从属关系管理。如果你工作在底层Java框架上,偶然引用到高层的GUI模板组件,这时代码不需要编译。这是由于构建文件在编译底层框架时在源路径中没有包含高层GUI面板组件的代码。

7.定义并重用文件路径
如果文件路径在一个地方一次性集中定义,并在整个构建文件中得到重用,那么构建文件更易于理解。以下是这样做的一个例子:

<projectname="sample"default="compile"basedir=".">
<pathid="classpath.common">
<pathelementlocation="${jdom.jar.withpath}"/>
...etc
</path>
<pathid="classpath.client">
<pathelementlocation="${guistuff.jar.withpath}"/>
<pathelementlocation="${another.jar.withpath}"/>
<!--reusethecommonclasspath-->
<pathrefid="classpath.common"/>
</path>
<targetname="compile.common"depends="prepare">
<javacdestdir="${dir.build}"srcdir="${dir.src}">
<classpathrefid="classpath.common"/>
<includename="com/oreilly/common/**"/>
</javac>
</target>
</project>
当项目不断增长构建日益复杂时,这一技术越发体现出其价值。你可能需要为编译不同层次的应用定义各自的文件路径,比如运行单元测试的、运行应用程序的、运行Xdoclet的、生成JavaDocs的等等不同路径。这种组件化路径定义的方法比为每个任务单独定义路径要优越得多。否则,很容易丢失任务从属关系的轨迹。

8.定义恰当的任务从属关系
假设dist任务从属于jar任务,那么哪个任务从属于compile任务哪个任务从属于prepare任务呢?Ant构建文件最终定义了任务的从属关系图,它必须被仔细地定义和维护。

应该定期检查任务的从属关系以保证构建工作得到正确执行。大的构建文件随着时间推移趋向于增加更多的任务,所以到最后可能由于不必要的从属关系导致构建工作非常困难。比如,你可能发现在程序员只需编译一些没有使用EJB的GUI代码时又重新生成了EJB代码。

以“优化”的名义忽略任务的从属关系是另一种常见的错误。这种错误迫使程序员为了得到恰当的结果必须记住并按照特定的顺序调用一串任务。更好的做法是:提供描述清晰的公共任务,这些任务包含正确的任务从属关系;另外提供一套“专家”任务让你能够手工执行个别的构建步骤,这些任务不提供完整的构建过程,但是让那些专家用户在快速而恼人的编码期间能够跳过某些步骤。

9.使用属性
任何需要配置或可能发生变化的信息都应作为Ant属性定义下来。对于在构建文件中多次出现的值也同样处理。属性既可以在构建文件头部定义,也可以为了更好的灵活性而在单独的属性文件中定义。以下是在构建文件中定义属性的样式:

<projectname="sample"default="compile"basedir=".">
<propertyname="dir.build"value="build"/>
<propertyname="dir.src"value="src"/>
<propertyname="jdom.home"value="../java-tools/jdom-b8"/>
<propertyname="jdom.jar"value="jdom.jar"/>
<propertyname="jdom.jar.withpath"
value
="${jdom.home}/build/${jdom.jar}"/>
etc...
</project>
或者你可以使用属性文件:

<projectname="sample"default="compile"basedir=".">
<propertyfile="sample.properties"/>
etc...
</project>
在属性文件sample.properties中:

dir.build=build
dir.src=src
jdom.home=../java-tools/jdom-b8
jdom.jar=jdom.jarjdom.jar.withpath=${jdom.home}/build/${jdom.jar}
用一个独立的文件定义属性是有好处的,它可以清晰地定义构建中的可配置部分。另外,在开发者工作在不同操作系统的情况下,你可以在不同的平台上提供该文件的不同版本。

10.保持构建过程独立
为了最大限度的扩展性,不要应用外部路径和库文件。最重要的是不要依赖于程序员的CLASSPATH设置。取而代之的是,在构建文件中使用相对路径并定义自己的路径。如果你引用了绝对路径如C:java ools,其他开发者未必使用与你相同的目录结构,所以就无法使用你的构建文件。

如果你部署开放源码项目,应该提供包含编译代码所需的所有JAR文件的发行版本。当然,这是在遵守许可协议的基础上。对于内部项目,相关的JAR文件都应在版本控制系统的管理中,并捡出(checkout)到大家都知道的位置。

当你必须引用外部路径时,应将路径定义为属性。使程序员能够用适合他们自己的机器环境的参数重载这些属性。你也可以使用以下语法引用环境变量:

<property</sp
分享到:
评论

相关推荐

    ANT全套资料20100322

    "ant使用笔记.ziw"和"Ant入门教程.ziw"是个人实践和学习心得,通常会包含作者在实际使用过程中遇到的问题及解决方案,对于解决实际问题非常有帮助。而".ziw"文件通常是知网或类似平台的下载格式,需要相应的阅读器...

    Ant.The.Definitive.Guide2

    10. **最佳实践**:有效的Ant使用技巧包括合理组织构建文件、避免硬编码路径、利用属性文件进行配置,以及编写清晰、可维护的构建脚本。 在压缩包中,"www_sj00_com.txt"可能是一个文本文件,包含Ant的使用示例或...

    ant maven3

    4. **Maven3实战笔记-书签版-《jianggq工作室》.pdf**:这是一本关于Maven3的实战教程,可能包含了Maven3的使用技巧、最佳实践和常见问题解决,适合Maven初学者和进阶者阅读。 5. **Mavenʵס.pdf**:这个文件名可能...

    appfuse 学习笔记

    开发指南详细讲解了AppFuse的核心组件和最佳实践。在持久层(PERSISTENCE)中,AppFuse利用Java Persistence API (JPA)注解来定义实体类,并通过Maven的生命周期来生成对应的数据库表。Hibernate作为默认的ORM解决...

    CMake.Practice.读书笔记.尽量使用外部构建.二进制文件存储目录切换1

    CMake是一种跨平台的构建系统,它以CMakeLists.txt文件作为配置文件,简化了构建和编译过程,尤其适合管理大型项目。CMake的特点包括开源、跨平台以及能够管理复杂项目。相比于直接编写makefile,CMake的语法更加...

    J2EE 资源集合

    通过深入学习这些文档,你不仅可以掌握J2EE的基本技术,还能了解到实际开发中的最佳实践。这些资料对于J2EE开发者来说是一份宝贵的资源,可以帮助你在项目开发中更加得心应手。记得不断学习和实践,才能在快速发展的...

    maven window下安装包

    第5章:坐标和依赖/5.9 最佳实践/5.9.2 依赖属性使用变量 第5章:坐标和依赖/5.9 最佳实践/5.9.3 依赖关系查看 第6章:仓库/6.1 何为Maven仓库 第6章:仓库/6.2 仓库的布局 第6章:仓库/6.3 仓库的分类 第6章:仓库/...

    jpetstore-3-1-1

    《jpetstore-3-1-1:一个Java EE电子商务示例应用的探索》 "jpetstore-3-1-1"是一个经典的Java EE应用示例,它由Apache Struts项目...通过对这个项目的研究,我们可以掌握到实际Web应用开发的许多关键技术和最佳实践。

    weblogic的用法

    总之,WebLogic是一个功能丰富的应用服务器,它的部署、配置、管理和优化都需要深入了解其特性和最佳实践。通过与Oracle数据库的紧密集成,WebLogic能够为企业提供稳定、高效且可扩展的应用平台。在实践中,不断学习...

    commons-codec-1.3-src.tar.gz

    标题“commons-codec-1.3-src.tar.gz”揭示了这是一个源代码压缩包,...对于开发者来说,通过阅读源代码和这些配置文件,可以理解Apache Commons Codec库的工作原理,学习编码最佳实践,并可能为项目贡献自己的代码。

    一个提供常用工具如urlencodedecodemarkdown等的Web应用

    总的来说,这个Web应用是一个典型的React全栈项目,展示了现代前端开发的最佳实践,包括组件化、状态管理和路由。对于想深入学习React及其生态系统的人来说,这是一个很好的学习资源。同时,对于需要快速使用URL编码...

    junit.rar_Java编程_Java_

    "Junit学习笔记.mht"可能是一位经验丰富的开发者对JUnit使用的心得体会,包含了一些实战中的技巧和最佳实践,如如何编写可读性高的测试代码,如何组织测试结构,以及如何利用JUnit与其他工具(如IDEs)集成等。...

    博客列表:我的博文列表,按系列分类,方便查找。

    通过这个博客列表,我们可以期待深入学习Android开发的最佳实践、Gradle构建技巧、Jetpack组件的使用方法,以及Kotlin语言的高级特性。这些内容对于提升Android开发者的技能和理解现代Android架构都是极其宝贵的资源...

Global site tag (gtag.js) - Google Analytics