
1. 从零起步先搞清楚这套工具链到底在干什么Android 加 Kotlin 这两个词摆在一起基本就是当下安卓原生开发的标准起手式。我早年用 Java 写过不少 Activity后来官方把 Kotlin 扶正为一级语言新开的工程我基本不再考虑 Java 了。这些年我带过几个刚入行的人发现他们卡住的地方往往不是 Kotlin 语法本身而是前面那一堆工具装不明白、Gradle 一同步就红一片、真机连不上、包打不出来。其实这些东西单独看都不难难的是没人告诉你它们之间是什么关系于是第一次动手就像在雾里走路。这篇就按我自己的实操顺序从安装 Android Studio、创建第一个 Android Kotlin 应用讲到一个能点、能存数据、能装到手机上的小成品中间那些文档里一笔带过、实际会绊人的地方我都会点出来。先说一个我反复强调的观念不要一上来就背语法也不要先把 Kotlin 面试题刷一遍。第一个项目的目的不是学会所有东西而是把编辑—构建—安装—运行—调试这条链路走通一遍。链路通了后面学什么都是往这条管线上挂插件链路不通学再多语法也只是纸上谈兵。我见过太多人收藏了一堆 Kotlin 中文文档结果连一个可以运行的工程都没跑起来热情就这么耗没了。1.1 Android Studio、SDK、JDK、Gradle 四者的关系很多人第一次装环境会懵是因为界面上同时出现了 Android Studio、Android SDK、JDK、Gradle 四个名字不知道谁管谁。我用一句话概括Android Studio 是车间JDK 是电力Android SDK 是原材料和机床Gradle 是流水线的调度员。Android Studio 本身是基于 IntelliJ IDEA 社区版做的 IDE负责写代码、给提示、跑调试器、管理模拟器和设备它是你唯一需要主动打开的东西。它内部捆绑了一个 JetBrains Runtime也就是一份定制过的 JDK所以现在的安装包基本都是自带电力的你不需要再单独去 Oracle 官网折腾 JDK。Android SDK 是一整套工具和库的集合你会在 SDK Manager 里看到几类东西platforms目录下是各个 API 级别的android.jar也就是你编译时对照的标准答案build-tools里有把资源编译打包的aapt2、把字节码转换压缩的d8/r8platform-tools里有我们后面要反复用的adb。这些东西缺一个构建就可能报错。Gradle 是构建系统负责把源码、资源、依赖、签名一步步组装成 APK 或 AAB。但 Gradle 自己不认识安卓所以还需要一个叫 AGP(Android Gradle Plugin) 的插件来告诉它安卓项目该怎么构建。这三者有严格的版本对应关系对不上就是满屏红字这也是新手最常踩的坑组件当前主流版本关键约束Android Studio2024 及以后版本决定能用的 AGP 上限AGP8.x8.x 要求 JDK 17、Gradle 8.xGradle8.x由 wrapper 锁定通常不用手动装JDK17低于 17 会在同步阶段直接失败记住一条Gradle 的版本不要自己去装项目里的gradle/wrapper/gradle-wrapper.properties已经写死了这个项目该用哪个版本点同步时它会自己下载。手动装一个全局 Gradle 反而容易造成版本打架。1.2 安装过程中的几个关键选择去 Android Studio 官网下载这件事本身没什么可讲的选对应系统版本、点下一步就行。倒是安装过程中有几个细节我吃过亏值得单独拎出来说。第一个是路径。SDK 的安装路径绝对不要包含中文和空格。我第一次装的时候图省事放在我的文档下面结果 Gradle 同步时反复报路径解析错误排查了一下午才发现是路径里有个中文目录名。把它挪到D:\Android\Sdk这种纯英文短路径下世界立刻清净了。第二个是安装类型。安装向导会问你是 Standard 还是 Custom新手直接选 Standard 就好它会帮你把 SDK、platform-tools、模拟器镜像都下好。唯一的代价是下载量大如果网速一般可以在 SDK Manager 里把不需要的 API 级别和系统镜像去掉只留下当前要用的那一个。第三个是汉化。这个被问得太多了。现在 Android Studio 官方市场里就有中文语言包路径是Settings → Plugins → Marketplace搜索Chinese就能看到一个简体中文语言包插件装上重启界面就变中文了。注意是官方市场里的那个不要从乱七八糟的地方下所谓汉化包覆盖安装目录那种做法升级一次 IDE 就会出问题。我个人的建议是菜单汉化没问题但代码里的 API 名字、报错信息还是英文早点习惯英文报错反而学得更快。第四个是初次启动的 SDK 配置。进Settings → Languages Frameworks → Android SDK把SDK Platforms里最新的两三个 API 勾上SDK Tools里把Android SDK Build-Tools、Android SDK Platform-Tools、Android SDK Command-line Tools勾上。命令行工具这一项特别容易被忽略但很多自动化脚本和 CI 配置都依赖它。提示如果你的网络环境下载 SDK 很慢可以在 SDK Manager 里找镜像地址设置或者在init.gradle里把仓库地址统一改成国内的镜像源。这个配置写一次之后所有项目都会生效比每个项目改一遍build.gradle省事得多。2. 创建工程模板选择与 Gradle 配置的门道环境齐了接下来就是 New Project。这一步看起来只是点几下但里面藏着不少要理解的东西尤其是模板生成了哪些文件、这些文件各自负责什么。我见过不少人做了半年项目还是不敢动build.gradle一改就崩本质上是没搞清这几个文件的分工。2.1 Empty Activity 模板到底生成了什么在 New Project 界面你会看到 Phone and Tablet 下面有一排模板Empty Activity、Empty Views Activity、Basic Views Activity、Bottom Navigation Activity 等等。现在的新版本里Empty Activity 默认用的是 Jetpack Compose 那套声明式 UI而 Empty Views Activity 才是传统的 XML 布局。这两条路差别很大新手容易选错方向。我的建议是如果你打算系统学安卓先把 Views 这条路走通因为它对应的 XML 布局、findViewById、RecyclerView这些概念在现有的存量项目和面试里出现频率依然极高等你对生命周期、资源系统、事件分发有了概念再上 Compose会顺很多。反过来先学 Compose遇到自定义 View、老项目迁移就会发懵。选完模板点 Finish工程会开始第一次 Gradle Sync这是最慢的一次因为要下载依赖。同步完成后左侧目录大致是这样几块app/目录一个典型的应用模块你的代码、资源、清单文件都在这。多模块项目里会有多个这样的模块。app/src/main/java/Kotlin 源码目录包名会自动按你填的域名倒序生成。app/src/main/res/资源目录layout放布局values放字符串和颜色drawable放图片mipmap放应用图标。app/src/main/AndroidManifest.xml应用的户口本声明 Activity、权限、应用图标和标签。app/build.gradle.kts这个模块的构建脚本依赖、compileSdk、签名配置都在这。build.gradle.kts(项目根)全局配置一般只放插件声明。settings.gradle.kts声明参与构建的模块和仓库地址。gradle.properties全局的构建参数比如开启 AndroidX、调整内存。gradle/wrapper/gradle-wrapper.properties锁定这个项目用的 Gradle 版本。理解这张清单你就知道改一个依赖该去哪个文件、加一个权限该去哪、换图标该动哪里。新手最常见的一个错误是把依赖加到了项目根的build.gradle.kts里结果app模块里根本用不了。记住跟代码相关的依赖加在app/build.gradle.kts的dependencies块里。2.2 Kotlin DSL 与 Groovy DSL 的区别与选型如果你看过几年前的老教程build.gradle里写的是apply plugin: com.android.application这种不带括号的奇特语法那是 Groovy DSL。而现在的模板默认生成.kts后缀的文件用的是 Kotlin DSL。这两者的区别被问得很多我按自己的理解梳理一下。Groovy DSL 的优点是语法宽松、写起来短网上老资料一大片缺点是 IDE 基本给不了补全和类型检查写错一个字母只有同步失败时才知道。Kotlin DSL 恰好相反它是强类型的你在dependencies块里敲一个库名IDE 能提示、能跳转、能标红配合 Gradle 的配置缓存增量构建也更快。代价是语法更啰嗦有些写法必须显式声明类型。维度Groovy DSLKotlin DSL文件后缀.gradle.gradle.kts类型检查运行时才报错编译期就能发现IDE 补全几乎没有完整支持构建性能一般配置缓存下更快网上资料存量多新增为主迁移成本—需要把语法改写我实际的做法是新项目一律用 Kotlin DSL老项目不动它除非有明确收益。硬要迁移最容易出错的地方是ext变量和属性赋值的写法Groovy 里compileSdk 34这种省略等号的调用在 Kotlin DSL 里必须写成compileSdk 34很多人漏了这个等号就同步失败。2.3 几个必须搞懂的构建参数app/build.gradle.kts里有几个参数看着简单但选错了后果不小。我把它们的作用和取值逻辑讲清楚compileSdk是编译时使用的 SDK 版本你应该选当前最新或次新的稳定版本。它决定了你能调用哪些新 API但不会影响运行因为这只是编译期的对照表。minSdk是应用能安装的最低系统版本。这个值的选取是一道取舍题调高能用更多新特性、少写兼容代码但会丢用户调低覆盖面广但要处理更多兼容分支。我一般看数据决定如果产品没有特殊要求定在 24 或 26 是比较省事的区间能把绝大多数还在用的设备覆盖住又躲开了一些古老的兼容坑。targetSdk表示你针对哪个版本做过适配测试。系统会用这个值决定要不要给你套上某些行为变更。它和minSdk的关系是minSdk ≤ targetSdk ≤ compileSdk。每次系统发新版本把targetSdk往上抬之前一定要去读官方的行为变更说明因为有些变更会直接让你的旧代码失效比如后台限制、存储权限、通知权限的调整。注意targetSdk不是为了上架才改的数字它实实在在影响运行时行为。我曾经遇到过一个后台任务在新版本上突然不执行排查半天最后发现就是targetSdk提升后系统收紧了后台限制代码本身没任何问题。3. 写第一个界面从布局到交互环境通了、工程建好了接下来写点能看见的东西。我习惯用一个小计数器作为第一个练手项目因为它足够简单却覆盖了安卓开发最核心的几件事布局、控件查找、事件监听、状态保存。麻雀虽小五脏俱全。3.1 用 XML 描述界面长什么样res/layout/activity_main.xml是界面的图纸。我把它换成一个垂直排列的布局包含一个显示数字的文本、一个进度条、两个按钮?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:gravitycenter android:padding24dp TextView android:idid/tvCount android:layout_widthwrap_content android:layout_heightwrap_content android:textSize48sp android:text0 / ProgressBar android:idid/progressBar style?android:attr/progressBarStyleHorizontal android:layout_widthmatch_parent android:layout_heightwrap_content android:max10 android:layout_marginTop16dp / Button android:idid/btnAdd android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop24dp android:text加一 / Button android:idid/btnReset android:layout_widthmatch_parent android:layout_heightwrap_content android:text重置 / /LinearLayout这里有几个尺寸单位要分清dp用于控件大小和间距它会根据屏幕密度自动换算保证不同手机上物理大小接近sp用于字体它还会跟随用户的系统字体设置缩放所以文字一定要用sp用dp会导致用户调大字体时界面不变体验很差。这个细节写进新项目的第一天比后面改一堆布局强。再一个是android:id后面代码要通过它找到控件。命名习惯上我推荐用控件类型缩写做前缀像tvCount、btnAdd、progressBar在几十个控件的页面里一眼就能找到目标。3.2 用 ViewBinding 代替 findViewById老教程里找控件的写法是findViewByIdTextView(R.id.tvCount)写多了又长又容易出错还要处理可空类型。现在推荐用 ViewBinding它是官方提供的、编译期生成绑定类的方式安全且不用手写查找。开启方式是在app/build.gradle.kts的android块里加一行android { // ... 其他配置 buildFeatures { viewBinding true } }同步之后每个布局文件都会生成一个同名的绑定类activity_main.xml对应ActivityMainBinding。Activity 里的写法变成这样package com.example.firstkotlinapp import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.example.firstkotlinapp.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private var count 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.btnAdd.setOnClickListener { count refreshUi() } binding.btnReset.setOnClickListener { count 0 refreshUi() } refreshUi() } private fun refreshUi() { binding.tvCount.text count.toString() binding.progressBar.progress count.coerceAtMost(10) } }lateinit var表示这个属性稍后初始化告诉编译器我心里有数别逼我在声明时赋值。它在 Activity 这种有明确生命周期的地方很合适因为binding一定会在onCreate里被赋值之后的点击回调里访问它不会为空。coerceAtMost(10)是 Kotlin 提供的一个很顺手的方法意思是最多取到 10超过就按 10 算省掉了自己写if判断。3.3 几个必会的 Kotlin 语法点写这个小项目会用到几个 Kotlin 的高频语法我顺便讲清楚它们为什么这么设计比死记硬背管用。空安全。Kotlin 把类型分成可空和不可空两种String不能为 nullString?才可以。这是它最值钱的设计之一把空指针异常从运行期提前到了编译期。实操里你会经常遇到三种操作符?.表示左边为空就整体返回空不继续调用?:表示左边为空就用右边的默认值!!表示我确定它不为空但一旦真的为空就会崩所以我在正式项目里尽量避免用它除非能百分百确定。字符串模板。上面代码里count.toString()其实可以写成$count这就是字符串模板。如果表达式复杂一点用花括号包起来比如总计 ${count 1} 次。它比 Java 里的字符串拼接清爽太多日常写日志、拼提示文字基本离不开。字符串格式化。需要控制小数位或者补零的时候用String.format()。比如把计数显示成三位数val text String.format(%03d, count)%03d的意思是整数不足三位前面补零。Kotlin 里它底层还是调 Java 的String.format所以格式占位符的写法和 Java 完全一致。因为它是 Java 的方法在 Kotlin 里参数为 null 时不会走 Kotlin 的空检查这是个容易忽略的小坑传参前最好确认一下。when 表达式。它是switch的升级版既能当语句也能当表达式返回值而且不需要写break不会出现忘记 break 导致的穿透问题。这几个点掌握了写一个页面的交互基本不会卡住。剩下的就是多写。4. 让数据活下来用 SharedPreferences 存一点东西计数器有个明显的毛病退出应用再进来数字归零了。要让它记住上次的数值得做持久化。轻量级的键值对存储SharedPreferences 是最直接的选择。4.1 SharedPreferences 的基本用法与封装SharedPreferences 本质是一个 XML 文件用键值对的方式存少量数据适合保存开关状态、上次登录的用户名、简单的计数这类信息。拿到它的方式有两种一种是按名字拿一个独立的文件一种是直接用 Activity 默认的val sp getSharedPreferences(app_config, Context.MODE_PRIVATE)MODE_PRIVATE表示这个文件只有本应用能读写这也是现在唯一推荐的模式其他几个模式要么废弃要么涉及跨应用共享的安全风险不要用。读写用起来非常直白// 写入 sp.edit().putInt(count, count).apply() // 读取第二个参数是找不到时的默认值 val saved sp.getInt(count, 0)这里有个新手必踩的点apply()和commit()的区别。commit()是同步写入会阻塞当前线程直到写盘完成返回一个布尔值表示成功与否apply()是先把改动写到内存然后异步落盘不返回结果。在主线程里一律用apply()用commit()写大数据时会引起界面卡顿。只有在极少数需要确认写入结果的场景下才考虑commit()而且那种场景也应该放到子线程。把上面这套接进 Activity 的思路是在onCreate里读一次恢复上次的值在每次计数变化后写一次。但每次都直接调sp.edit()写一遍代码会散落在各处。我习惯做一层薄封装class ConfigStore(context: Context) { private val sp context.getSharedPreferences(app_config, Context.MODE_PRIVATE) var count: Int get() sp.getInt(KEY_COUNT, 0) set(value) sp.edit().putInt(KEY_COUNT, value).apply() companion object { private const val KEY_COUNT count } }用 Kotlin 的属性委托式写法外面用起来就像普通字段一样configStore.count 5可读性好键名集中管理也不容易写错。这是我个人比较推荐的写法比到处传递SharedPreferences对象干净得多。注意SharedPreferences 存的是明文任何能拿到设备文件的人都能读到里面的内容。所以密码、令牌这类敏感信息不要往里放需要本地加密存储的应该用专门的加密方案。4.2 什么时候不该用 SharedPreferencesSharedPreferences 好用但不是万能。它有几个明确的边界越界使用会给你埋雷。第一数据量大的时候不要用。它在apply()时会把整个文件的内容读进内存文件越大读写的代价越高而且这些内容会一直占着内存。如果你要存几百条记录它就不合适了。第二结构化数据不要用。它只能存基本类型和字符串集合存一个对象列表就得自己序列化成 JSON 字符串塞进去读的时候再反序列化用起来别扭性能也差。第三不要拿它当数据库。需要按条件查询、排序、分页的应该用 Room。需要存配置流、希望读的时候能感知变化的现在更推荐 DataStore它同样是键值对但基于协程和 Flow异步、类型安全还不会像 SharedPreferences 那样在主线程做磁盘操作。我自己的判断标准很简单几对键值、体积小、能接受每次都全量读写用 SharedPreferences只要有一个条件不满足就往上换 DataStore 或 Room。把它当成一个便利贴而不是档案柜。5. 真机调试与常见问题排查代码写完得让它跑起来。模拟器能跑大部分场景但真机的表现才是最终标准特别是涉及性能、蓝牙、传感器这些功能时模拟器基本帮不上忙。5.1 ADB 常用命令速查ADB(Android Debug Bridge) 是连接电脑和设备的桥梁它本身在 SDK 的platform-tools目录下。真机调试的前提是打开开发者选项里的 USB 调试插上线后在设备上确认授权。第一次连接通常要在手机上点一次允许调试并且勾选始终允许否则每次插拔都要重新授权。几个我每天都会用到的命令# 查看当前连接的设备unauthorized 表示还没在手机上点同意 adb devices # 安装一个 APK-r 表示覆盖安装保留数据 adb install -r app-debug.apk # 卸载-k 表示保留数据 adb uninstall com.example.firstkotlinapp # 抓日志按标签过滤看自己应用的输出 adb logcat -s MainActivity:D # 清空日志缓冲排查前先清一次更清爽 adb logcat -c # 进设备 shell可以看文件、查进程 adb shell # 杀掉应用进程方便测试冷启动 adb shell am force-stop com.example.firstkotlinappadb devices显示unauthorized是最常见的问题解决办法是先确认手机屏幕上是否弹出了授权对话框如果没有可以在开发者选项里点一下撤销 USB 调试授权然后拔插一次线。如果还是不行换个 USB 口、换根数据线很多所谓的连不上其实是线只能充电不能传数据。5.2 构建与安装常见报错速查我把这几年被问得最多、自己也踩过的坑整理成一张表遇到的时候直接对照现象常见原因处理思路Gradle sync failed网络导致依赖没下全换仓库镜像重试同步必要时删缓存Unsupported class file major versionJDK 版本与 AGP 不匹配在设置里把 Gradle JDK 切到 17SDK location not found换电脑后local.properties丢了新建该文件并写入sdk.dir路径INSTALL_FAILED_UPDATE_INCOMPATIBLE手机上已装了签名不同的同包名应用先卸载旧版本再装真机列表为空驱动、线材或授权问题换线换口撤销授权重连项目在别人电脑上打不开Gradle 版本、JDK 版本或 SDK 路径不一致统一版本检查local.properties应用图标不更新用了mipmap缓存或没改清单确认清单里的图标引用清一次构建说到项目移植这是团队协作里高频出问题的地方。把工程从一台电脑拷到另一台最容易丢的就是local.properties因为它通常被.gitignore排除掉里面存着本机的 SDK 路径。新机器上要么让 IDE 自动生成要么手动写一行sdk.dirD\:\\Android\\Sdk。另外gradle/wrapper目录一定要一起拷过去它是保证所有人用同一个 Gradle 版本的关键。至于那几个 Gradle 缓存目录反而不用拷那是可再生的。提示项目里如果出现三角形模糊箭头这类显示异常多半是图标资源本身的问题比如用了矢量图但缺少必要的属性声明或者图片分辨率不足被拉伸。图标建议统一用矢量资源一张图适配所有密度比准备五套 PNG 省事。5.3 看懂报错信息的正确姿势新手看到一屏红字就慌其实报错信息是有层级的。Gradle 的输出通常是一个主错误 一堆被牵连的次生错误真正有用的往往是最上面那条带Caused by的。我的习惯是先滚到最顶上看第一个错误把它的关键词复制去搜而不是从下往上一条条看。另外日志抓取也有技巧。Log.d()打出来的东西在logcat里混着系统和其他应用的海量信息用标签过滤能立刻聚焦。我的做法是给自己的类名做标签或者直接用TAG常量抓日志时adb logcat -s TAG:D只看自己的输出效率高得多。发布版本里记得把调试日志去掉一来避免泄露信息二来减少性能开销。6. 打包出第一个能装的安装包调试包跑通之后最后一步是打一个可以发给别人安装的正式包。这一步的门槛在于签名很多人卡在这里其实是没理解签名的意义。6.1 签名的意义与生成流程Android 要求每个安装包都必须签名签名的核心作用是证明这个包确实来自同一个开发者。系统用签名来判断两个包能不能互相覆盖安装、能不能共享数据。所以同一个应用的所有版本必须用同一个签名文件一旦换了签名老用户就无法直接升级必须先卸载。生成签名文件用 JDK 自带的keytoolkeytool -genkeypair -v \ -keystore my-release.jks \ -alias mykey \ -keyalg RSA \ -keysize 2048 \ -validity 10000执行后会让你输入密码和一堆主体信息。这里的几个参数值得说明-keyalg RSA -keysize 2048是当前普遍推荐的密钥算法和长度-validity 10000是有效期天数一万天大约二十七年。填写时的密码和别名一定要记牢尤其是.jks文件本身要备份好丢了基本等于这个应用再也无法更新。生成之后在app/build.gradle.kts里配置签名信息不要把密码明文写在构建文件里提交到仓库应该放在local.properties或环境变量里读取。这是很多团队出过安全事故的地方。打包路径有两处Build → Generate Signed Bundle / APK。它会让你选 AAB 还是 APK。AAB 是上传到应用商店用的格式商店会根据设备配置拆出更小的安装包APK 是可以直接分发安装的格式。自己测试用 APK 更方便上架用 AAB。6.2 顺手看一眼包体积和签名指纹打完包可以用Build → Analyze APK打开产物看看每个部分占了多少体积。这一步在新手阶段看着多余但养成习惯后你能很快发现某个库莫名其妙占了好几兆这类问题。签名指纹也就是 SHA1是很多第三方服务接入时要填的东西。查看方式keytool -list -v -keystore my-release.jks -alias mykey输出里会列出 MD5、SHA1、SHA256 等指纹。注意调试包和正式包用的是不同签名文件指纹是不一样的接入第三方服务时要用正式签名的那一套否则上线后会验证失败。这个坑我见过不止一个团队踩过——开发阶段一切正常一上正式包功能就失效原因就是填了调试签名的指纹。至于包体积优化我一般做三件事开启 R8 代码压缩和资源压缩、去掉没用到的库、把大图换成矢量或压缩格式。但这些都是后话第一个项目不必纠缠于此先把链路跑通最重要。我在带新人的过程中最大的感受是第一个 Android Kotlin 应用的价值不在于它有多完善而在于它逼你把整条链路走了一遍。环境装好那一刻、Gradle 第一次同步成功那一刻、真机上图标亮起来那一刻这些通了的瞬间才是真正让你入门的东西。后面再学 Jetpack、学架构、学性能优化都是在一条已经打通的管线上加东西而不是对着一堆零散的知识点发愁。我个人的习惯是每学一个新东西就把它塞进这个计数器小项目里试试项目慢慢就长成了自己的知识仓库比单纯看文档记得牢得多。