嘿,朋友,坐稳了。写Android应用这事儿,看着挺吓人,好像要学Java、Kotlin、XML、Gradle、Jetpack Compose……一堆名词往脑袋里砸。但只要你把那个“Hello World”拆开揉碎了看,你会发现,原来它也没那么高冷。
今天咱们不整那些教科书式的“第一章、第二章”,我就当个老大哥,跟你唠唠这些年我踩过的坑、写过的代码,以及那些让初学者(甚至中级开发者)抓狂的坑爹瞬间。
第一章:别被“Hello World”骗了,它其实不简单
很多人第一行代码都是 Log.d("TAG", "Hello World")。完了,感觉行了,我可以统治世界了。
打住。
真正的Hello World不是这一行日志。它是——如何在屏幕上打出一行字,并且这行字在屏幕旋转、键盘弹出、内存不足时还能稳稳地待在那儿。
传统View方式的“坑”
如果你还在用XML写布局,你的第一个Activity可能长这样:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 你以为你拿到了TextView?
val tv = findViewById<TextView>(R.id.hello_text)
// 这时候tv可能是null,如果XML里ID写错了,或者fragment还没inflate完
tv.text = "Hello, 复杂世界"
}
}
常见问题:NullPointerException 或 IllegalStateException
你有没有遇到过这种报错:
java.lang.IllegalStateException: Activity has been destroyed
或者更常见的:
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.TextView.setText(...)' on a null object reference
真相是: 你以为findViewById一定返回你绑定的View,但有时候(比如Fragment生命周期混乱、ViewTreeObserver异步回调)它返回null。
解法:用ViewBinding或Kotlin synthetics(虽然synthetics被弃用了,但大家懂那个意思)。
// 使用ViewBinding,推荐
private var _binding: ActivityMainBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 绝对安全,因为Binding在inflate成功后才可用
binding.helloText.text = "Hello, 安全世界"
}
override fun onDestroy() {
super.onDestroy()
_binding = null // 防止内存泄漏
}
第二章:生命周期——Android的“生死簿”
这是最难的部分。不是技术难,是脑子难。
你需要记住:onCreate、onStart、onResume、onPause、onStop、onDestroy。
但现实比这残酷得多。
场景:你正在输入密码,屏幕亮了又灭,来电了,然后你按了Home键。
你的App经历了什么?
graph LR
A[onCreate] --> B[onStart]
B --> C[onResume]
C --> D[onPause - 来电]
D --> E[onStop - 按Home]
E --> F[onDestroy - 被系统回收]
F --> G[onCreate - 用户返回]
坑爹时刻: 你以为在onPause里保存数据就安全了?错。如果系统内存极度紧张,它可能直接杀进程,onStop甚至onDestroy都不执行。
正确姿势:用onSaveInstanceState
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// 这里保存的数据,在onCreate或onRestoreInstanceState时能拿到
outState.putString("username", etUsername.text.toString())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
// 这是从“死亡”中复活,不是第一次启动
val savedName = savedInstanceState.getString("username")
etUsername.setText(savedName)
} else {
// 这是真·全新启动
}
}
记住:onSaveInstanceState不是onPause的替代品。它是“系统要杀我但我还能抢救一下”的最后机会。
第三章:线程与主线程——Android的“交通规则”
Android有一条铁律:主线程(UI线程)不能干重活。
你以为你只是下载个图片?结果ANR(Application Not Responding)来了。
异步加载的正确姿势
1. 老派:AsyncTask(已弃用,但你可能还在维护的代码里看到)
// 别用了!真的别用了
new AsyncTask<Void, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(Void... params) {
return loadImageFromNetwork(url);
}
@Override
protected void onPostExecute(Bitmap result) {
imageView.setImageBitmap(result);
}
}.execute();
2. 现代派:Coroutines(协程)—— 强烈推荐
class ImageFragment : Fragment() {
private val viewModel: ImageViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 自动在主线程运行,因为observe是在主线程调用
viewModel.image.observe(viewLifecycleOwner) { bitmap ->
imageView.setImageBitmap(bitmap)
}
// 点击加载,触发协程
button.setOnClickListener {
viewModel.loadImage("https://example.com/image.jpg")
}
}
}
class ImageViewModel : ViewModel() {
private val _image = MutableLiveData<Bitmap?>()
val image: LiveData<Bitmap?> = _image
fun loadImage(url: String) {
viewModelScope.launch(Dispatchers.IO) { // 在IO线程执行
val bitmap = downloadBitmap(url)
withContext(Dispatchers.Main) { // 回到主线程更新UI
_image.value = bitmap
}
}
}
private fun downloadBitmap(url: String): Bitmap {
// 网络请求代码
return...
}
}
为什么协程好?
- 不会泄露上下文(ViewModel生命周期管理)
- 代码顺序写,异步执行,可读性极强
- 不需要写各种Callback,层层嵌套
第四章:Jetpack Compose——未来的写法
如果你现在开始学Android,请直接从Compose开始。不要先学XML再学Compose,那是走弯路。
Compose vs XML:直观对比
XML写法:
<!-- activity_main.xml -->
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello" />
// MainActivity.kt
val tv = findViewById<TextView>(R.id.text)
tv.text = "New Hello" // 命令式:告诉系统“做什么”
Compose写法:
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!") // 声明式:告诉系统“是什么”
}
@Preview
@Composable
fun DefaultPreview() {
Greeting("Android")
}
点击事件也超简单:
Button(onClick = {
// 点击后做什么,直接写这里
viewModel.onClick()
}) {
Text("点我")
}
状态管理:Compose的灵魂
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text("$count")
Button(onClick = { count++ }) {
Text("增加")
}
}
}
注意:remember 很重要。 它确保状态在重组(Recomposition)时保留,而不是每次都重置为0。
第五章:常见问题“急救箱”
问题1:Gradle同步失败,报“Could not resolve com.android.tools.build:gradle:8.0.0”
症状: 下载依赖卡住,或者版本不匹配。
解决:
- 检查
gradle-wrapper.properties里的distributionUrl版本是否和build.gradle里的classpath一致。 - 设置镜像:在
build.gradle里加阿里云镜像。
repositories {
google()
mavenCentral()
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/public' }
}
问题2:App闪退,Logcat里只有一行“FATAL EXCEPTION: main”
症状: 没有详细信息,或者信息被折叠。
解决:
- 展开Logcat,找
Caused by:。 - 90%的情况是:
- 空指针:View还没 inflate 完就操作。
- 类型转换错误:
Intent拿到的数据类型不对。 - ConcurrentModificationException:在遍历List时修改了它。
技巧: 用?.和?:操作符链式调用,避免NPE。
// 危险
user.name.uppercase()
// 安全
user?.name?.uppercase() ?: "Unknown"
问题3:内存泄漏,App用着用着就OOM(Out of Memory)
症状: 切换页面多次后,App崩溃。
解决:
- Rule 1:
Activity、Context不要存为静态变量。 - Rule 2: 监听器(Listener)在
onDestroy/onPause中解绑。 - Rule 3: 用LeakCanary(现在默认集成在调试包中)。
// 错误示范:静态Activity引用
object Singleton {
var activity: Activity? = null // 内存泄漏!
}
// 正确示范:使用WeakReference或ViewModel
class MyViewModel : ViewModel() {
// ViewModel本身不会泄漏Context
fun loadData() { ... }
}
第六章:从“能跑”到“好跑”——性能优化初探
1. 图片加载:别用imageView.setImageResource()加载网络图
用Glide或Coil。
// Coil(Compose友好)
AsyncImage(
model = ImageRequest.Builder(context)
.data("https://example.com/image.jpg")
.crossfade(true)
.build(),
contentDescription = null,
modifier = Modifier.size(200.dp)
)
2. RecyclerView优化
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
// 1. 使用viewBinding
class MyViewHolder(val binding: ItemMyBinding) : RecyclerView.ViewHolder(binding.root)
// 2. 设置固定大小,如果知道item高度不变
override fun getItemViewType(position: Int): Int = R.layout.item_my
// 3. 减少onBindViewHolder里的耗时操作
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
// 只绑定数据,不要做网络请求
holder.binding.tvTitle.text = dataList[position].title
}
}
3. 数据库:用Room,别用原生SQL
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String
)
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAll(): Flow<List<User>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
}
Flow 自动观察数据变化,UI自动刷新,不用手动notifyDataSetChanged。
第七章:真实案例——做一个“待办事项”App
让我们把前面的知识串起来。
1. 数据层(Repository + Room)
class TodoRepository(private val todoDao: TodoDao) {
val allTodos: Flow<List<Todo>> = todoDao.getAll()
suspend fun addTodo(todo: Todo) {
todoDao.insert(todo)
}
}
2. 业务层(ViewModel)
class TodoViewModel(private val repository: TodoRepository) : ViewModel() {
val todos: StateFlow<List<Todo>> = repository.allTodos.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
fun addTodo(text: String) {
viewModelScope.launch {
repository.addTodo(Todo(id = System.currentTimeMillis(), text = text, done = false))
}
}
}
3. 展示层(Compose UI)
@Composable
fun TodoScreen(viewModel: TodoViewModel = viewModel()) {
val todos by viewModel.todos.collectAsStateWithLifecycle()
var inputText by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
// 输入框
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
label = { Text("新待办") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// 添加按钮
Button(
onClick = {
if (inputText.isNotBlank()) {
viewModel.addTodo(inputText)
inputText = ""
}
},
modifier = Modifier.fillMaxWidth()
) {
Text("添加")
}
// 列表
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(todos) { todo ->
TodoItem(todo = todo)
}
}
}
}
@Composable
fun TodoItem(todo: Todo) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = todo.done, onCheckedChange = null)
Text(
text = todo.text,
modifier = Modifier.padding(start = 8.dp),
textDecoration = if (todo.done) TextDecoration.LineThrough else TextDecoration.None
)
}
}
看,整个App结构清晰:数据、逻辑、UI完全分离。修改UI不影响数据,修改数据不影响UI逻辑。
结语:这不是终点
写Android应用,就像在城市里开车。开始时你怕闯红灯(NPE),后来你怕刹车失灵(OOM),再后来你怕迷路(架构混乱)。
但只要你:
- 尊重生命周期
- 别在主线程干重活
- 用现代工具(Coroutines, Compose, ViewModel)
- 多查官方文档(developer.android.com)
你迟早能成为那个在地铁上随手画出复杂App架构的大牛。
现在,打开Android Studio,创建一个新项目,选“Empty Compose Activity”,开始你的第一次旅程吧。记住,第一个Hello World很简单,但下一个Hello World,会更精彩。
祝你代码无Bug,发版零事故!🚀
