Android开发入门到实战:从零开始打造你的第一个APP
一、先别急着写代码,让我给你泼盆冷水
说真的,我在接触编程之前,也以为开发APP就是打开一个编辑器,噼里啪啦敲几行代码,然后一个炫酷的应用就出现了。后来才知道,这就像以为吃了颗药片就能变成武林高手一样天真。
但好消息是,Android开发其实比你想的简单多了,尤其是Google早就把坑都填得差不多了。你今天看到这篇文章,说明你已经迈出了第一步——这步很多成年人走了十年都没迈出去。
咱们今天不搞那些教科书式的”什么是Android”,直接从你能动手的地方开始。我会带你从零搭建环境,学会写界面,跟服务器打交道,最后让你的APP跑得飞快。整篇内容足够你从头到尾跟着做,保证你看完能自己做出一个能用的APP。
二、工欲善其事,必先利其器
下载并安装Android Studio
Android Studio是Google官方推出的开发工具,就像作家需要Word一样,你想写Android APP就得用它。
打开浏览器,访问 developer.android.google.cn/studio,下载适合你系统的版本。安装过程比较简单,一路”下一步”就行。不过要注意,Google服务在某些网络环境下可能需要梯子,如果下载慢,可以用国内镜像站。
配置你的第一个开发环境
安装完成后,第一次打开Android Studio会有个配置向导。这里有个关键点:一定要勾选”Android Virtual Device”,这是虚拟手机的意思。你不用真买一台手机,电脑上就能跑。
如果家里网络不好,可以跳过这一步,后面再手动配置。
创建一个新项目
打开Android Studio,点击”Start a new Android Studio project”,然后会看到一堆模板。新手推荐选“Empty Activity”,也就是空项目,这样你能从头理解每个文件是干嘛的。
填好以下信息:
- Name:起个你喜欢的名字,比如”MyFirstApp”
- Package name:用倒序域名格式,比如
com.yourname.myfirstapp - Save location:项目保存的位置
- Language:选 Kotlin(现在主流语言,Java也可以,但Kotlin更简洁)
- Minimum SDK:选 API 21(Android 5.0,覆盖95%以上的设备)
点Finish,等Gradle同步完成(首次可能需要几分钟,耐心等)。
三、认识你的第一个界面
项目创建成功后,你会看到一堆文件和文件夹。别慌,咱们只看最重要的几个:
目录结构速览
app/
├── src/main/
│ ├── java/com/yourname/myfirstapp/
│ │ └── MainActivity.kt # 主界面代码
│ ├── res/
│ │ ├── layout/
│ │ │ └── activity_main.xml # 界面布局文件
│ │ └── values/
│ │ └── colors.xml # 颜色定义
│ └── AndroidManifest.xml # 应用清单文件
└── build.gradle # 项目构建配置
第一个界面长什么样?
打开 res/layout/activity_main.xml,你会看到类似这样的代码:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
这就是你的第一个界面。它很简单,只有一个居中的”Hello World!“文字。
用代码控制界面
打开 MainActivity.kt:
package com.yourname.myfirstapp
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
// 声明控件变量
private lateinit var textView: TextView
private lateinit var button: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 加载界面布局
setContentView(R.layout.activity_main)
// 找到界面中的控件
textView = findViewById(R.id.textView)
button = findViewById(R.id.button)
// 设置按钮点击事件
button.setOnClickListener {
textView.text = "按钮被点击了!"
Toast.makeText(this, "你好,Android开发!", Toast.LENGTH_SHORT).show()
}
}
}
代码里的每一行都有它的作用,我来给你解释:
setContentView(R.layout.activity_main):告诉APP用哪个布局文件findViewById():找到界面上的具体控件setOnClickListener:给按钮添加点击事件Toast.makeText():弹出一个小提示框
理解约束布局(ConstraintLayout)
新手最容易卡住的地方是布局。ConstraintLayout是Android推荐的布局方式,它用”约束”来定位控件。
比如这个代码:
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
意思是:把这个控件的上下左右四个边,都约束到父容器的对应边上,结果就是居中显示。
你可以用Android Studio的可视化编辑器来调整布局,像搭积木一样拖拽控件,编辑器会自动生成代码。建议新手先用可视化编辑器熟悉,再用代码精调。
四、让界面动起来:常见控件的使用
常用控件一览
开发APP离不开这些控件:
| 控件 | 作用 | 常用属性 |
|---|---|---|
| TextView | 显示文字 | text, textSize, textColor |
| Button | 按钮 | text, onClick |
| EditText | 输入框 | hint, inputType |
| ImageView | 显示图片 | src, scaleType |
| LinearLayout | 线性布局 | orientation, gravity |
| RecyclerView | 列表 | adapter, layoutManager |
| Switch | 开关 | checked |
| ProgressBar | 进度条 | visibility, max |
做个小练习:计算器界面
咱们来个实用的例子,做一个简单的计算器界面:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="#F5F5F5">
<!-- 显示结果的文本框 -->
<TextView
android:id="@+id/resultText"
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="0"
android:textSize="48sp"
android:textColor="#333333"
android:gravity="end|center_vertical"
android:background="#FFFFFF"
android:padding="16dp"/>
<!-- 按钮区域 -->
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="4"
android:rowCount="5"
android:layout_marginTop="16dp">
<!-- 第一行:清除、删除、除法、乘法 -->
<Button android:text="C" android:id="@+id/btnClear" />
<Button android:text="⌫" android:id="@+id/btnDelete" />
<Button android:text="/" android:id="@+id/btnDivide" />
<Button android:text="×" android:id="@+id/btnMultiply" />
<!-- 第二行:7、8、9、减法 -->
<Button android:text="7" />
<Button android:text="8" />
<Button android:text="9" />
<Button android:text="-" android:id="@+id/btnSubtract" />
<!-- 第三行:4、5、6、加法 -->
<Button android:text="4" />
<Button android:text="5" />
<Button android:text="6" />
<Button android:text="+" android:id="@+id/btnAdd" />
<!-- 第四行:1、2、3、等号 -->
<Button android:text="1" />
<Button android:text="2" />
<Button android:text="3" />
<Button android:text="=" android:id="@+id/btnEqual"
android:background="#FF9800"
android:textColor="#FFFFFF"/>
<!-- 第五行:0(占两格)、小数点 -->
<Button android:text="0"
android:layout_columnSpan="2"
android:layout_gravity="fill"/>
<Button android:text="." />
</GridLayout>
</LinearLayout>
对应的Kotlin代码:
package com.yourname.myfirstapp
import android.os.Bundle
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
class CalculatorActivity : AppCompatActivity() {
private lateinit var resultText: TextView
private var currentInput = ""
private var operator = ""
private var firstNumber = 0.0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_calculator)
resultText = findViewById(R.id.resultText)
// 数字按钮点击事件
for (i in 0..9) {
val button = findViewById<Button>(getButtonId(i))
button.setOnClickListener {
currentInput += i.toString()
resultText.text = currentInput
}
}
// 小数点
findViewById<Button>(R.id.btnDot).setOnClickListener {
if (!currentInput.contains(".")) {
currentInput += "."
resultText.text = currentInput
}
}
// 运算符按钮
findViewById<Button>(R.id.btnAdd).setOnClickListener { handleOperator("+") }
findViewById<Button>(R.id.btnSubtract).setOnClickListener { handleOperator("-") }
findViewById<Button>(R.id.btnMultiply).setOnClickListener { handleOperator("×") }
findViewById<Button>(R.id.btnDivide).setOnClickListener { handleOperator("÷") }
// 等号
findViewById<Button>(R.id.btnEqual).setOnClickListener { calculate() }
// 清除
findViewById<Button>(R.id.btnClear).setOnClickListener { clear() }
// 删除最后一位
findViewById<Button>(R.id.btnDelete).setOnClickListener {
if (currentInput.isNotEmpty()) {
currentInput = currentInput.dropLast(1)
resultText.text = if (currentInput.isEmpty()) "0" else currentInput
}
}
}
private fun handleOperator(op: String) {
if (currentInput.isNotEmpty()) {
firstNumber = currentInput.toDouble()
operator = op
currentInput = ""
}
}
private fun calculate() {
if (currentInput.isNotEmpty() && operator.isNotEmpty()) {
val secondNumber = currentInput.toDouble()
var result = 0.0
when (operator) {
"+" -> result = firstNumber + secondNumber
"-" -> result = firstNumber - secondNumber
"×" -> result = firstNumber * secondNumber
"÷" -> result = if (secondNumber != 0.0) firstNumber / secondNumber else 0.0
}
resultText.text = result.toString().trimEnd('0').trimEnd('.')
currentInput = result.toString()
operator = ""
}
}
private fun clear() {
currentInput = ""
operator = ""
firstNumber = 0.0
resultText.text = "0"
}
private fun getButtonId digit: Int): Int {
return when (digit) {
0 -> R.id.btn0
1 -> R.id.btn1
2 -> R.id.btn2
3 -> R.id.btn3
4 -> R.id.btn4
5 -> R.id.btn5
6 -> R.id.btn6
7 -> R.id.btn7
8 -> R.id.btn8
9 -> R.id.btn9
else -> R.id.btn0
}
}
}
这个例子涵盖了:
- 界面布局:使用GridLayout排列按钮
- 事件处理:给每个按钮绑定点击事件
- 逻辑编写:实现计算器的基本功能
五、数据交互:让APP学会”说话”
什么是数据交互?
你的APP不是孤岛,它需要从网络获取数据,或者把数据发送到服务器。这就像你给朋友发微信,也需要网络一样。
Android开发中最常用的网络请求方式有几种:
- OkHttp:轻量级、高性能,最推荐
- Retrofit:基于OkHttp,更优雅,适合大型项目
- Volley:Google官方出品,适合简单的请求
咱们先用OkHttp,简单直接。
添加网络权限和依赖
打开 AndroidManifest.xml,在 <application> 标签前添加:
<uses-permission android:name="android.permission.INTERNET" />
然后打开 build.gradle (Module: app),在 dependencies 中添加:
dependencies {
// OkHttp
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
// Gson(用于解析JSON)
implementation 'com.google.code.gson:gson:2.10.1'
// Coroutines(协程,异步处理必备)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}
添加后,点击”Sync Now”同步项目。
写一个网络请求
假设我们要从GitHub获取用户信息:
package com.yourname.myfirstapp.network
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
// 网络请求类
class NetworkClient {
private val client = OkHttpClient()
// 获取用户信息
suspend fun getUserInfo(username: String): UserInfo? {
return withContext(Dispatchers.IO) {
val url = "https://api.github.com/users/$username"
val request = Request.Builder()
.url(url)
.build()
try {
val response: Response = client.newCall(request).execute()
if (response.isSuccessful) {
val body = response.body?.string() ?: return@withContext null
parseUserInfo(body)
}
} catch (e: Exception) {
e.printStackTrace()
}
null
}
}
// 解析JSON数据
private fun parseUserInfo(json: String): UserInfo? {
return try {
val jsonObject = JSONObject(json)
UserInfo(
login = jsonObject.optString("login"),
name = jsonObject.optString("name"),
followers = jsonObject.optInt("followers"),
avatarUrl = jsonObject.optString("avatar_url")
)
} catch (e: Exception) {
null
}
}
}
// 数据模型类
data class UserInfo(
val login: String?,
val name: String?,
val followers: Int,
val avatarUrl: String?
)
在界面中展示数据
package com.yourname.myfirstapp
import android.os.Bundle
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.yourname.myfirstapp.network.NetworkClient
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var editText: EditText
private lateinit var button: Button
private lateinit var resultText: TextView
private val networkClient = NetworkClient()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
editText = findViewById(R.id.editText)
button = findViewById(R.id.button)
resultText = findViewById(R.id.resultText)
button.setOnClickListener {
val username = editText.text.toString().trim()
if (username.isNotEmpty()) {
fetchData(username)
} else {
Toast.makeText(this, "请输入用户名", Toast.LENGTH_SHORT).show()
}
}
}
private fun fetchData(username: String) {
// 显示加载状态
resultText.text = "加载中..."
button.isEnabled = false
lifecycleScope.launch {
val userInfo = networkClient.getUserInfo(username)
runOnUiThread {
button.isEnabled = true
if (userInfo != null) {
resultText.text = """
用户名:${userInfo.login}
姓名:${userInfo.name}
粉丝:${userInfo.followers}
""".trimIndent()
} else {
resultText.text = "未找到用户"
}
}
}
}
}
这段代码的核心是协程(Coroutines)。Android的UI线程不能做耗时操作(比如网络请求),否则APP会卡死。协程让我们可以写看起来像同步的代码,但实际在后台线程执行。
lifecycleScope.launch 是Lifecycle组件提供的协程作用域,当Activity销毁时会自动取消任务,避免内存泄漏。
六、数据持久化:让数据留下来
网络数据获取到了,但如果用户重启APP,数据就没了。这时候需要持久化存储。
方式一:SharedPreferences(存简单数据)
适合存用户名、设置等小数据:
package com.yourname.myfirstapp.storage
import android.content.Context
import android.content.SharedPreferences
class PreferencesManager(context: Context) {
private val prefs: SharedPreferences =
context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
// 保存数据
fun saveString(key: String, value: String) {
prefs.edit().putString(key, value).apply()
}
// 读取数据
fun getString(key: String, defaultValue: String = ""): String {
return prefs.getString(key, defaultValue) ?: defaultValue
}
// 删除数据
fun remove(key: String) {
prefs.edit().remove(key).apply()
}
// 清除所有数据
fun clearAll() {
prefs.edit().clear().apply()
}
}
使用示例:
// 保存登录状态
val prefs = PreferencesManager(this)
prefs.saveString("username", "张三")
prefs.saveString("token", "abc123")
// 读取登录状态
val username = prefs.getString("username")
方式二:Room数据库(存复杂数据)
适合存用户信息、商品列表等结构化数据。Room是Google官方的数据库库,比SQLite好用得多。
首先在 build.gradle 中添加依赖:
dependencies {
def room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
// 协程支持
implementation("androidx.room:room-ktx:$room_version")
}
然后创建实体类:
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val age: Int,
val email: String
)
创建数据访问对象(DAO):
import androidx.room.*
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(users: List<User>)
@Query("SELECT * FROM users ORDER BY id DESC")
suspend fun getAllUsers(): List<User>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("DELETE FROM users")
suspend fun deleteAll()
}
创建数据库:
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
使用示例:
// 初始化数据库
val db = AppDatabase.getDatabase(this)
val userDao = db.userDao()
// 添加用户
lifecycleScope.launch {
userDao.insert(User(name = "张三", age = 25, email = "zhangsan@example.com"))
}
// 查询所有用户
lifecycleScope.launch {
val users = userDao.getAllUsers()
// 更新UI
}
方式三:文件存储
适合存图片、大文件等:
import android.content.Context
import java.io.File
class FileStorageManager(context: Context) {
// 保存到内部存储
fun saveToFile(fileName: String, content: String): Boolean {
return try {
val file = File(context.filesDir, fileName)
file.writeText(content)
true
} catch (e: Exception) {
false
}
}
// 从内部存储读取
fun readFile(fileName: String): String? {
return try {
val file = File(context.filesDir, fileName)
file.readText()
} catch (e: Exception) {
null
}
}
// 保存到外部存储(需要权限)
fun saveToExternal(fileName: String, content: String): Boolean {
return try {
val file = File(context.getExternalFilesDir(null), fileName)
file.writeText(content)
true
} catch (e: Exception) {
false
}
}
}
七、性能优化:让你的APP飞起来
为什么需要性能优化?
用户打开APP超过3秒就会觉得卡,超过5秒基本就关掉了。性能优化不是炫技,是用户体验的基本要求。
优化一:图片加载
图片是APP性能的杀手。直接用 ImageView 加载网络图片会导致卡顿。
推荐使用 Glide 或 Coil 库:
dependencies {
// Glide
implementation 'com.github.bumptech.glide:glide:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
// 或者 Coil(Kotlin推荐)
implementation 'io.coil-kt:coil:2.5.0'
}
使用Glide:
import com.bumptech.glide.Glide
// 加载网络图片
Glide.with(this)
.load("https://example.com/image.jpg")
.placeholder(R.drawable.loading) // 加载中的占位图
.error(R.drawable.error) // 加载失败的错误图
.into(imageView)
// 加载圆形图片
Glide.with(this)
.load("https://example.com/avatar.jpg")
.circleCrop()
.into(avatarImageView)
// 加载本地图片
Glide.with(this)
.load(R.drawable.local_image)
.into(imageView)
使用Coil(Kotlin协程版本):
import coil.load
// 加载网络图片
imageView.load("https://example.com/image.jpg") {
placeholder(R.drawable.loading)
error(R.drawable.error)
crossfade(true) // 渐变动画
}
// 加载异步任务
lifecycleScope.launch {
val imageLoader = ImageLoader(this@MainActivity)
val result = imageLoader.execute(ImageRequest.Builder(this@MainActivity)
.data("https://example.com/image.jpg")
.build())
imageView.image = result.drawable
}
优化二:RecyclerView优化
ListView是老旧的列表控件,现在推荐用 RecyclerView。
dependencies {
implementation 'androidx.recyclerview:recyclerview:1.3.2'
}
创建列表项布局 item_user.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="centerCrop"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="12dp">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"/>
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="#666666"/>
</LinearLayout>
</LinearLayout>
创建适配器:
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.yourname.myfirstapp.databinding.ItemUserBinding
class UserAdapter(
private val users: List<User>
) : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {
// ViewHolder,缓存控件引用
class UserViewHolder(private val binding: ItemUserBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(user: User) {
binding.name.text = user.name
binding.description.text = user.description
// 使用Glide加载头像
Glide.with(binding.avatar.context)
.load(user.avatarUrl)
.circleCrop()
.into(binding.avatar)
}
}
// 创建ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val binding = ItemUserBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return UserViewHolder(binding)
}
// 绑定数据
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(users[position])
}
// 返回数据数量
override fun getItemCount() = users.size
}
在Activity中使用:
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class UserListActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: UserAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user_list)
recyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
// 设置适配器
adapter = UserAdapter(sampleUsers)
recyclerView.adapter = adapter
}
companion object {
val sampleUsers = listOf(
User("张三", "开发者", "https://example.com/1.jpg"),
User("李四", "设计师", "https://example.com/2.jpg"),
User("王五", "产品经理", "https://example.com/3.jpg"),
// 更多数据...
)
}
}
优化三:内存泄漏防护
内存泄漏是新手最容易忽视的问题。泄漏多了,APP会卡顿甚至崩溃。
常见泄漏场景和解决方案:
// ❌ 错误:静态引用Context
class BadExample {
companion object {
private var context: Context? = null // 泄漏!
}
}
// ✅ 正确:使用Application Context
class GoodExample(application: Application) {
private val context = application.applicationContext
}
// ❌ 错误:匿名内部类持有Activity引用
button.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
// this@MainActivity被持有,Activity无法被回收
}
})
// ✅ 正确:使用Lambda,注意生命周期
button.setOnClickListener {
// 在Activity销毁前确保取消
}
// ❌ 错误:Handler未移除
class BadHandlerExample(activity: Activity) {
private val handler = Handler(Looper.getMainLooper())
init {
handler.postDelayed(Runnable {
// 执行任务
}, 1000)
}
}
// ✅ 正确:使用Lifecycle的repeatOnLifecycle
class GoodHandlerExample(activity: Activity) {
fun startObserving() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 只在STARTED状态后执行
while (true) {
// 定期检查
delay(1000)
}
}
}
}
}
优化四:布局优化
复杂的布局会让渲染变慢。
<!-- ❌ 不推荐:嵌套过深 -->
<LinearLayout>
<LinearLayout>
<LinearLayout>
<TextView />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- ✅ 推荐:使用ConstraintLayout扁平化 -->
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
使用 layout:tag 合并重复布局:
<!-- ❌ 重复代码 -->
<LinearLayout ...>
<include layout="@layout/item_header" />
<include layout="@layout/item_content" />
<include layout="@layout/item_footer" />
</LinearLayout>
<!-- ✅ 合并为单一布局 -->
<merge>
<TextView ... />
<TextView ... />
<TextView ... />
</merge>
优化五:使用Profile GPU Rendering调试
在开发者选项中开启”GPU渲染模式”,可以看到每帧的渲染时间。超过16ms(60fps)就说明有性能问题。
也可以在代码中添加性能监控:
import android.os.SystemClock
import android.util.Log
class PerformanceMonitor {
companion object {
private const val TAG = "PerfMonitor"
fun measureBlock(name: String, block: () -> Unit) {
val start = SystemClock.uptimeMillis()
block()
val duration = SystemClock.uptimeMillis() - start
if (duration > 16) {
Log.w(TAG, "$name 耗时 ${duration}ms")
}
}
}
}
// 使用
PerformanceMonitor.measureBlock("网络请求") {
val data = networkClient.fetchData()
}
八、实战:做一个完整的待办事项APP
光说不练假把式,咱们做个完整的APP:待办事项清单。
功能设计
- 添加待办事项
- 标记完成/未完成
- 删除事项
- 数据持久化(Room数据库)
- 下拉刷新
创建项目
按照前面的步骤创建项目,命名为 “TodoApp”。
创建数据模型
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "todos")
data class Todo(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val completed: Boolean = false,
val createdAt: Long = System.currentTimeMillis()
)
创建DAO和Database
import androidx.room.*
@Dao
interface TodoDao {
@Query("SELECT * FROM todos ORDER BY createdAt DESC")
suspend fun getAllTodos(): List<Todo>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(todo: Todo): Long
@Update
suspend fun update(todo: Todo)
@Delete
suspend fun delete(todo: Todo)
@Query("UPDATE todos SET completed = :completed WHERE id = :id")
suspend fun toggleTodo(id: Int, completed: Boolean)
}
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import android.content.Context
@Database(entities = [Todo::class], version = 1, exportSchema = false)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao(): TodoDao
companion object {
@Volatile
private var INSTANCE: TodoDatabase? = null
fun getDatabase(context: Context): TodoDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TodoDatabase::class.java,
"todo_database"
).build()
INSTANCE = instance
instance
}
}
}
}
创建Adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.yourname.myfirstapp.databinding.ItemTodoBinding
import com.yourname.myfirstapp.Todo
class TodoAdapter(
private val onToggle: (Todo) -> Unit,
private val onDelete: (Todo) -> Unit
) : ListAdapter<Todo, TodoAdapter.TodoViewHolder>(TodoDiffCallback()) {
class TodoViewHolder(private val binding: ItemTodoBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(todo: Todo, onToggle: (Todo) -> Unit, onDelete: (Todo) -> Unit) {
binding.checkbox.isChecked = todo.completed
binding.titleText.text = todo.title
// 已完成的事项字体变灰
binding.titleText.textStyle = if (todo.completed) {
android.graphics.Typeface.ITALIC
} else {
android.graphics.Typeface.NORMAL
}
binding.titleText.alpha = if (todo.completed) 0.5f else 1.0f
binding.checkbox.setOnCheckedChangeListener { _, isChecked ->
onToggle(todo.copy(completed = isChecked))
}
binding.deleteButton.setOnClickListener {
onDelete(todo)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
val binding = ItemTodoBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return TodoViewHolder(binding)
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
holder.bind(getItem(position), onToggle, onDelete)
}
// DiffUtil用于高效更新列表
class TodoDiffCallback : DiffUtil.ItemCallback<Todo>() {
override fun areItemsTheSame(oldItem: Todo, newItem: Todo) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Todo, newItem: Todo) =
oldItem == newItem
}
}
主界面代码
import android.os.Bundle
import android.view.View
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.yourname.myfirstapp.databinding.ActivityMainBinding
import com.yourname.myfirstapp.Todo
import com.yourname.myfirstapp.TodoDatabase
import com.yourname.myfirstapp.TodoAdapter
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var adapter: TodoAdapter
private lateinit var database: TodoDatabase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
database = TodoDatabase.getDatabase(this)
// 设置RecyclerView
adapter = TodoAdapter(
onToggle = { todo -> toggleTodo(todo) },
onDelete = { todo -> deleteTodo(todo) }
)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
// 加载数据
loadTodos()
// 添加新事项
binding.addButton.setOnClickListener {
addTodo()
}
// 下拉刷新
binding.refreshLayout.setOnRefreshListener {
loadTodos()
}
}
private fun loadTodos() {
binding.progressBar.visibility = View.VISIBLE
binding.refreshLayout.isRefreshing = true
lifecycleScope.launch {
val todos = database.todoDao().getAllTodos()
adapter.submitList(todos)
binding.progressBar.visibility = View.GONE
binding.refreshLayout.isRefreshing = false
}
}
private fun addTodo() {
val title = binding.titleInput.text.toString().trim()
if (title.isEmpty()) {
Toast.makeText(this, "请输入待办事项", Toast.LENGTH_SHORT).show()
return
}
lifecycleScope.launch {
val todo = Todo(title = title)
database.todoDao().insert(todo)
binding.titleInput.text.clear()
loadTodos()
}
}
private fun toggleTodo(todo: Todo) {
lifecycleScope.launch {
database.todoDao().toggleTodo(todo.id, todo.completed)
loadTodos()
}
}
private fun deleteTodo(todo: Todo) {
lifecycleScope.launch {
database.todoDao().delete(todo)
loadTodos()
}
}
}
主界面布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- 标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="待办事项"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<!-- 输入区域 -->
<EditText
android:id="@+id/titleInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入新的待办事项..."
android:inputType="text"
android:maxLines="1"/>
<!-- 添加按钮 -->
<Button
android:id="@+id/addButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="添加"
android:layout_marginTop="8dp"
android:background="#4CAF50"
android:textColor="#FFFFFF"/>
<!-- 加载指示器 -->
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"/>
<!-- 列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"/>
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
列表项布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:background="?attr/selectableItemBackground">
<CheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"/>
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:textSize="16sp"
android:layout_marginStart="12dp"/>
<Button
android:id="@+id/deleteButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:text="✕"
android:textSize="18sp"
android:backgroundTint="#F44336"
android:padding="0"/>
</LinearLayout>
这个APP实现了完整的增删改查功能,而且是异步操作,不会卡顿。
九、打包发布:让你的APP跑到用户手机上
生成签名证书
发布前需要给APP签名,证明是你开发的。
在Android Studio中:
- 点击菜单 Build > Generate Signed Bundle / APK
- 选择 Android App Bundle(推荐)或 APK
- 创建或选择密钥库
- 填写证书信息(别名、密码、有效期等)
- 选择构建类型(Release)
- 点击Finish
生成的 .aab 文件就是可以发布到Google Play的包。
配置发布参数
打开 build.gradle (Module: app),添加签名配置:
android {
signingConfigs {
release {
storeFile file("../keystore.jks")
storePassword "your_password"
keyAlias "your_alias"
keyPassword "your_password"
}
}
buildTypes {
release {
minifyEnabled true // 代码混淆
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}
上传到应用商店
Google Play:
- 创建开发者账号(需要25美元一次性费用)
- 访问
play.google.com/console - 点击”创建应用”
- 上传
.aab文件 - 填写应用信息(名称、描述、截图等)
- 设置定价和发行范围
- 提交审核
国内应用商店:
- 华为应用市场
- 小米应用商店
- OPPO软件商店
- vivo应用商店
- 腾讯应用宝
每个商店都有自己的审核规范,需要提前阅读。
十、学习路线和进阶方向
新手学习路径
基础阶段(1-2周)
- 掌握Kotlin基础语法
- 熟悉常用控件和布局
- 理解Activity生命周期
进阶阶段(2-4周)
- 学习RecyclerView、Fragment
- 掌握网络请求和数据处理
- 学会使用Room数据库
实战阶段(4-8周)
- 做一个完整的APP项目
- 学习MVVM架构模式
- 掌握依赖注入(Hilt/Dagger)
精通阶段(3个月+)
- Jetpack Compose(声明式UI)
- 协程和Flow高级用法
- 性能调优和架构设计
推荐学习资源
- 官方文档:developer.android.com(最权威)
- Kotlin官方教程:kotlinlang.org/docs/home.html
- Android Developers YouTube频道:有最新技术分享
- GitHub开源项目:看别人怎么写代码
常见坑和解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| APP崩溃 | 空指针异常 | 使用?.和?:安全操作符 |
| 界面卡顿 | UI线程做耗时操作 | 使用协程/线程池 |
| 内存泄漏 | 持有Activity引用 | 使用Application Context |
| 布局错位 | 约束设置错误 | 用Layout Inspector调试 |
| 网络请求失败 | 没有权限或网络问题 | 检查权限,添加错误处理 |
十一、写在最后
从这篇文章开始,你已经有了从零到一开发Android APP的能力。不要指望看一遍就能精通,编程和学游泳一样,得自己动手做。
建议你接下来的行动:
- 按照前面的步骤,创建一个项目并运行成功
- 修改示例代码,添加你自己的功能
- 做一个你真正想用的APP,哪怕很简单
- 遇到问题去Stack Overflow或官方文档找答案
记住,Google官方文档是最好的老师,比任何教程都权威。遇到问题时,先试着搜索错误信息,大部分问题别人都遇到过。
最后送你一句话:最好的学习方法是做项目。不要等”准备好了”再开始,边做边学才是正道。等你做出第一个能用的APP时,那种成就感会让你爱上编程。
加油,未来的Android开发者!
