本文还有配套的精品资源,点击获取
简介:MyApplication是一款基于安卓平台的手机应用程序,采用Java语言开发,专注于提供旅游指南服务。该应用集成了地图导航、景点信息、文化介绍、餐饮住宿推荐等功能,利用Android SDK中的Google Maps API实现定位与路线规划,并通过网络API或数据库获取实时旅游数据。应用界面使用XML布局设计,结合Button、TextView等控件实现用户交互,通过Activity与Intent机制完成页面跳转和数据传递。项目以APK格式打包发布,源码结构清晰,包含在“MyApplication-master”目录中,适用于学习安卓应用开发全流程,涵盖UI设计、功能实现、资源管理与版本控制等内容。
安卓应用开发环境搭建与核心组件实战
你有没有遇到过这样的场景:刚入手一个新项目,信心满满地打开 Android Studio,结果编译失败、模拟器卡死、日志满屏红色报错…… 🤯 别急,这几乎是每个安卓开发者都踩过的坑。真正的问题不在于“会不会用工具”,而在于 是否理解这些工具背后的运行机制 。
我们今天就来一次深度拆解——从环境配置到代码实现,从底层构建流程到实际调试技巧,把那些藏在IDE背后的“黑盒”彻底打开。准备好了吗?Let’s dive in!👇
开发环境不是点几下安装就行
很多人以为装个 Android Studio 就万事大吉了,其实不然。IDE只是个壳子,真正驱动整个开发流程的是背后那一整套 SDK + Gradle + ADB + Emulator 的生态组合。
为什么推荐Android Studio?
没错,它是官方亲儿子,但更重要的是它集成了:
- Gradle 构建系统 :自动管理依赖、编译、打包
- SDK Manager :一键下载不同版本的 Android API
- AVD 模拟器 :无需真机也能测试各种设备形态
- Layout Editor 可视化布局编辑器 :拖拽式 UI 设计
但这并不意味着你可以完全依赖图形界面。一旦出现问题(比如资源冲突、签名错误),你就必须深入到底层去排查。
// 示例:Java基础语法在Android中的典型应用
public class MainActivity extends App***patActivity {
private String appName = "旅游助手";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 绑定布局文件
Log.d("Lifecycle", "App: " + appName + " 启动");
}
}
这段代码看起来简单,但它已经包含了几个关键概念:
- App***patActivity :兼容性支持类,适配旧版 Android
- onCreate() :生命周期起点
- setContentView() :绑定 XML 布局
- Log.d() :输出调试信息
如果你连这些都不清楚,那后续的复杂逻辑只会让你越来越迷糊。
💡 小贴士 :不要跳过基础知识!哪怕你现在只想做个地图功能,也得先搞懂 Activity 是怎么被创建和销毁的。
SDK目录结构:不只是文件夹那么简单
Android SDK 并不是一个神秘莫测的东西,它其实就是一堆有组织的文件夹集合。但每一个目录都有其特定用途,理解它们能帮你快速定位问题。
| 目录名称 | 功能描述 |
|---|---|
platforms/ |
存放不同 Android 版本的 API,如 android-34 对应 Android 14 |
platform-tools/ |
包含 adb 、 fastboot 等调试工具 |
tools/ |
提供早期 GUI 工具(现在大多集成进 AS) |
build-tools/ |
编译 APK 所需工具集,包含 aapt2、D8、zipalign |
emulator/ |
模拟器执行文件及资源 |
sources/ |
可选下载项,提供 Android 框架源码用于学习 |
system-images/ |
真实操作系统镜像,供模拟器使用 |
当你看到 ***pileSdkVersion 34 这样的配置时,它实际上就是在告诉 Gradle:“请用 platforms/android-34/ 下的 jar 包来编译我的代码。”
android {
***pileSdk 34
defaultConfig {
applicationId "***.example.mapapp"
minSdkVersion 24
targetSdkVersion 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
🔍 逐行解析这个配置的意义 :
-
***pileSdk 34:编译时使用的 API 级别。允许调用最新的方法,但不会强制你在运行时必须使用该系统。 -
minSdkVersion 24:最低支持 Android 7.0(Nougat)。低于此系统的设备无法安装。 -
targetSdkVersion 34:目标运行环境。影响权限行为、通知渠道等运行时策略。 -
versionCode/versionName:内部版本号和用户可见版本名,发布更新时必须递增。
⚠️ 注意:
targetSdkVersion不等于兼容性保障!如果你设为 34,系统会启用更严格的权限模型(比如后台定位限制),你需要主动处理这些变化。
构建流程图:你的APK是怎么诞生的?
你以为点击“Run”按钮后,代码就直接变成 APK 了吗?Too young too simple!
实际上,整个构建过程是一条精密的流水线:
graph TD
A[Source Code & Resources] --> B(aapt2)
B --> C[***piled Resources]
D[Java/Kotlin Classes] --> E(D8)
E --> F[classes.dex]
C --> G(Linking with Linker)
F --> G
G --> H(zipalign)
H --> I[Final APK]
每一步都在 Gradle 的任务中留下了痕迹:
-
:app:mergeDebugResources→ 合并所有 res 文件 -
:app:***pileDebugJavaWithJavac→ 编译 Java 源码 -
:app:dexBuilder→ 使用 D8 将字节码转为 .dex -
:app:packageDebug→ 打包成 APK
你可以通过命令行查看某个 APK 的资源表:
aapt2 dump resources app-release.apk
输出结果类似这样:
Package Name: ***.example.mapapp
Type spec ID: 0x01 (drawable)
Entry ID: 0x0105 (ic_launcher)
Config: (default)
Value: res/drawable/ic_launcher.png
这在逆向分析或自动化测试脚本编写中非常有用。
ADB:连接真实世界的桥梁 🌉
ADB(Android Debug Bridge)是你与安卓设备之间的通信中枢。无论是真机还是模拟器,只要你想调试,就得靠它。
它的架构是典型的客户端-服务器模式:
-
adb client:运行在你的电脑上 -
adb server:管理连接状态 -
adbd daemon:运行在安卓设备上的守护进程
首次连接前,请务必开启手机的“开发者选项”和“USB调试”。否则会出现 unauthorized 状态。
adb devices
正常输出应该是:
List of devices attached
emulator-5554 device
如果显示 unauthorized ,请在手机屏幕上确认授权对话框 ✅
高频操作清单:每天都会用到的ADB命令
| 命令 | 作用 |
|---|---|
adb install app-debug.apk |
安装APK |
adb uninstall ***.example.mapapp |
卸载应用 |
adb logcat \| grep "MapFragment" |
查看特定标签的日志 |
adb shell am start -n ***.example.mapapp/.MainActivity |
启动指定Activity |
adb pull /data/data/***.example.mapapp/db.sqlite ./backup/ |
导出数据库 |
adb push config.json /data/data/***.example.mapapp/files/ |
推送配置文件 |
其中最强大的是 am (Activity Manager)和 pm (Package Manager)两个 shell 工具。
例如,想让地图应用自动获取位置权限:
adb shell pm grant ***.example.mapapp android.permission.A***ESS_FINE_LOCATION
再也不用手动点了,特别适合批量测试!
还有更酷的操作——模拟 GPS 位置:
tel*** localhost 5554 <<EOF
auth <your-auth-token>
geo fix 116.4074 39.9042
EOF
这对测试导航、LBS 功能简直是神器!
而且你还可以通过 Wi-Fi 调试,摆脱 USB 线束缚:
adb tcpip 5555
adb connect 192.168.1.100:5555
适用于车载设备、IoT 终端等不方便插拔的场景。
sequenceDiagram
participant DevPC as 开发电脑
participant ADBServer as ADB Server
participant Device as 安卓设备
DevPC->>ADBServer: adb devices
ADBServer->>Device: 连接握手
Device-->>ADBServer: 返回序列号与状态
ADBServer-->>DevPC: 列出在线设备
DevPC->>ADBServer: adb logcat
ADBServer->>Device: 请求日志流
Device-->>ADBServer: 实时发送log buffer
ADBServer-->>DevPC: 输出日志内容
这张图揭示了 ADB 请求响应的真实流程。虽然表面看起来很简单,但背后涉及 USB 协议、加密认证、多路复用等多项技术。了解这一点,有助于诊断连接超时、权限拒绝等问题。
模拟器 vs 真机:谁才是王者?
很多新手喜欢用模拟器开发,觉得方便快捷;而老鸟则坚持“只有真机才能反映真实体验”。
其实正确做法是: 双轨并行 。
模拟器的优势在哪?
- 快速切换系统版本(Android 8 到 14 自由切换)
- 精准控制硬件参数(RAM、CPU 核心数)
- 支持传感器模拟(GPS、加速度计、陀螺仪)
创建一个 Pixel 4 模拟器:
avdmanager create avd -n Pixel_4_API_34 \
-k "system-images;android-34;google_apis;x86_64" \
-d "pixel_4"
启动时优化网络性能:
emulator -avd Pixel_4_API_34 -***delay none -***speed full
你会发现地图加载速度明显提升 👍
而且模拟器支持多种方式注入地理位置:
- 手动拖动 Extended Controls 中的地图标记
- 使用 tel*** 发送
geo fix命令 - 通过 ADB 设置 mock location
但要注意:高版本 Android 对模拟位置有严格限制,需要手动在“开发者选项”中指定测试应用。
真机测试不可替代的原因
| 测试项 | 模拟器 | 真机 |
|---|---|---|
| 地图初始化 | ✅ | ✅ |
| Marker加载 | ✅ | ✅ |
| GPS精度 | ⚠️(模拟) | ✅ |
| 离线地图读写 | ⚠️ | ✅ |
| 多语言适配 | ✅ | ✅ |
特别是国产 ROM,经常移除 Google Play Services,导致 Maps API 直接挂掉 ❌
解决方案有哪些?
- 提前检测:
isGooglePlayServicesAvailable() - 引导用户安装 MicroG 开源替代
- 提供离线地图降级方案(OSMDroid + MBTiles)
所以结论很明确: 开发阶段用模拟器快速迭代,上线前一定要在多款真机上验证兼容性 。
Activity生命周期:别再随便泄露内存了!
你有没有遇到过这种情况:切出去回来看视频,回来发现画面卡住不动?或者地图 marker 全没了?
很可能就是因为你没管好 Activity 的生命周期 😬
四大状态流转图解
graph TD
A[用户启动Activity] --> B{系统回调}
B --> C[onCreate()]
C --> D[onStart()]
D --> E[onResume()]
E --> F[Activity进入前台可交互状态]
这是启动流程。
当用户离开时:
graph TD
A[用户按Home键] --> B[onPause()]
B --> C[onStop()]
C --> D[onDestroy()]
顺序刚好相反。
重点来了:这三个退出方法的作用完全不同!
| 方法 | 是否仍在前台 | 是否可见 | 推荐操作 |
|---|---|---|---|
onPause() |
否 | 部分可见 | 暂停动画、停止媒体播放 |
onStop() |
否 | 不可见 | 停止后台任务、注销监听器 |
onDestroy() |
否 | 不可见 | 清理成员变量、释放资源 |
举个例子,你在 onCreate() 注册了一个 LocationListener:
@Override
protected void onPause() {
super.onPause();
if (locationManager != null && locationListener != null) {
locationManager.removeUpdates(locationListener);
}
}
记得在 onPause() 里取消注册!否则即使页面关闭,GPS 仍在后台持续工作,耗电爆炸 🔥
而且注意: onDestroy() 并不保证一定被调用。极端低内存情况下,系统可能直接杀掉进程。因此重要数据应在 onSaveInstanceState() 中持久化。
如何监控生命周期?自定义基类了解一下
为了避免每个 Activity 都重复写日志,可以封装一个基类:
public abstract class BaseActivity extends App***patActivity {
private static final String LIFECYCLE_TAG = "ACTIVITY_LIFECYCLE";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
logLifecycle("onCreate", savedInstanceState);
}
private void logLifecycle(String methodName, Bundle bundle) {
String stateInfo = bundle != null ? "Restoring Instance State" : "New Instance";
Log.i(LIFECYCLE_TAG, getClass().getSimpleName() + " -> " + methodName + " | " + stateInfo);
}
private void logLifecycle(String methodName) {
logLifecycle(methodName, null);
}
}
然后所有 Activity 继承它:
public class MainActivity extends BaseActivity { ... }
配合 Logcat 过滤 ACTIVITY_LIFECYCLE ,就能清晰看到页面跳转全过程:
I/ACTIVITY_LIFECYCLE: MainActivity -> onCreate | New Instance
I/ACTIVITY_LIFECYCLE: DetailActivity -> onCreate | Restoring Instance State
是不是比盲调试强多了?😎
XML布局设计:别再嵌套LinearLayout了!
还在用多个 LinearLayout 套来套去?醒醒吧兄弟,那是2010年的做法了!
现在官方推荐的是 ConstraintLayout ,它可以做到:
- 扁平化结构(减少层级)
- 高性能渲染(避免过度测量)
- 支持百分比、Guideline、Barrier 等高级特性
看个例子:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.***/apk/res/android"
xmlns:app="http://schemas.android.***/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn_submit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="提交"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
四个约束属性一加,按钮自动居中, 无需任何嵌套容器 !
相比之下,传统方法至少要两层:
<RelativeLayout>
<Button android:layout_centerInParent="true" />
</RelativeLayout>
或者三层 LinearLayout:
<LinearLayout android:gravity="center">
<LinearLayout android:orientation="vertical">
<Button />
</LinearLayout>
</LinearLayout>
每一层都会增加测量和绘制开销,影响滑动流畅度。
graph TD
A[根布局] --> B[LinearLayout]
A --> C[RelativeLayout]
A --> D[ConstraintLayout]
B --> E[线性排列, 易嵌套]
C --> F[相对定位, 性能低]
D --> G[扁平结构, 高性能]
G --> H[支持Guideline/Bias]
G --> I[兼容旧布局迁移]
趋势很明显: ConstraintLayout 是未来 。
layout_weight 的正确打开方式
想让两个 EditText 平分宽度?很多人第一反应是设 android:layout_width="0dp" + layout_weight="1" 。
但你知道为什么要设 0dp 吗?
因为权重分配公式是:
实际宽度 = (剩余空间 × 权重 / 总权重) + 自身最小宽度
如果设为 wrap_content ,系统会先按内容宽度占位,剩下的才按比例分,结果就不准了。
正确姿势:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:hint="用户名" />
<EditText
android:layout_width="0dp"
android:layout_weight="1"
android:hint="密码" />
</LinearLayout>
如果你想做 7:3 分割,就把权重改成 7 和 3。
这种机制非常适合响应式布局,在手机和平板上都能自适应。
include标签:告别重复代码
有没有发现很多页面都有相同的标题栏?每次复制粘贴不仅麻烦,改起来更是灾难。
解决办法:用 <include> 复用公共模块!
先定义一个通用标题:
<!-- res/layout/toolbar_***mon.xml -->
<LinearLayout xmlns:android="http://schemas.android.***/apk/res/android"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?attr/colorPrimary"
android:gravity="center_vertical"
android:padding="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:text="默认标题" />
</LinearLayout>
然后在任意布局中引用:
<include
layout="@layout/toolbar_***mon"
android:id="@+id/include_toolbar" />
Java 中获取内部控件:
TextView titleView = findViewById(R.id.include_toolbar).findViewById(R.id.tv_title);
titleView.setText("旅游详情页");
这样改一处,全局生效,团队协作效率翻倍 💪
graph LR
A[公共布局 toolbar_***mon.xml] --> B[Activity A 引入]
A --> C[Activity B 引入]
A --> D[Fragment C 引入]
style A fill:#e0f7fa,stroke:#006064
style B fill:#f0f4c3,stroke:#827717
style C fill:#f0f4c3,stroke:#827717
style D fill:#f0f4c3,stroke:#827717
组件化思想就这么落地了。
核心控件三剑客:Button、TextView、EditText
这三个控件构成了绝大多数界面的基础骨架。但大多数人只知道基本用法,却不懂如何定制和优化。
Button美化实战
默认按钮太丑?来试试自定义背景 Drawable:
<!-- res/drawable/btn_primary_rounded.xml -->
<shape xmlns:android="http://schemas.android.***/apk/res/android"
android:shape="rectangle">
<solid android:color="#2196F3"/>
<corners android:radius="8dp"/>
<padding
android:left="16dp"
android:right="16dp"
android:top="12dp"
android:bottom="12dp"/>
</shape>
布局中引用:
<Button
android:id="@+id/btn_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="搜索景点"
android:textColor="#FFFFFF"
android:textSize="16sp"
android:background="@drawable/btn_primary_rounded" />
圆角+内边距,瞬间提升质感 ✨
代码中添加点击事件:
Button searchBtn = findViewById(R.id.btn_search);
searchBtn.setOnClickListener(v ->
Toast.makeText(this, "正在搜索附近景点...", Toast.LENGTH_SHORT).show()
);
建议加上防抖机制,防止用户手抖连点多次。
TextView玩转富文本
不仅能显示文字,还能加粗、换行、变色:
String htmlText = "<b>¥599起</b><br/>北京两日游<br/>含故宫+颐和园门票";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
tvDescription.setText(Html.fromHtml(htmlText, Html.FROM_HTML_MODE_LEGACY));
} else {
tvDescription.setText(Html.fromHtml(htmlText));
}
更高级的做法是用 SpannableStringBuilder:
SpannableStringBuilder spanText = new SpannableStringBuilder("限时优惠:");
spanText.append("立减100元");
int start = spanText.length() - 4;
int end = spanText.length();
spanText.setSpan(new ForegroundColorSpan(Color.RED), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tvPromotion.setText(spanText);
“100元”变成红色,促销感立马拉满!
EditText输入校验怎么做?
邮箱输入框标配:
<EditText
android:id="@+id/et_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入邮箱"
android:inputType="textEmailAddress"
android:maxLength="50" />
代码中校验:
submitBtn.setOnClickListener(v -> {
String input = emailInput.getText().toString().trim();
if (Patterns.EMAIL_ADDRESS.matcher(input).matches()) {
Toast.makeText(this, "邮箱格式正确", Toast.LENGTH_SHORT).show();
} else {
emailInput.setError("请输入有效的邮箱地址");
}
});
setError() 会在下方显示红字提示,用户体验友好。
想要实时反馈?加上 TextWatcher:
emailInput.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
submitBtn.setEnabled(Patterns.EMAIL_ADDRESS.matcher(s).matches());
}
});
输入即反馈,流畅度爆表 🚀
网络请求:别再主线程跑HTTP了!
Android 明确禁止在主线程进行网络操作,否则抛出 ***workOnMainThreadException 。
主流方案有四种:
| 库名 | 特点 | 适用场景 |
|---|---|---|
| Retrofit | 注解驱动,类型安全 | 中大型项目 |
| OkHttp | 高性能,可拦截 | 底层封装 |
| Volley | 轻量级,谷歌出品 | 小型请求 |
| HttpURLConnection | 原生支持 | 学习原理 |
推荐组合: Retrofit + OkHttp
先定义数据模型:
public class TravelSpot {
private String id;
private String name;
private String description;
private double latitude;
private double longitude;
private String imageUrl;
private int rating;
// getter/setter...
}
再定义接口:
public interface TravelApiService {
@GET("travel/spots")
Call<List<TravelSpot>> getSpotsByCity(@Query("city") String city);
@GET("travel/spot/{id}")
Call<TravelSpot> getSpotDetail(@Path("id") String spotId);
}
构建 Retrofit 实例:
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.addInterceptor(new HttpLoggingInterceptor().setLevel(BODY))
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.***/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
TravelApiService service = retrofit.create(TravelApiService.class);
发起请求:
service.getSpotsByCity("hangzhou").enqueue(new Callback<List<TravelSpot>>() {
@Override
public void onResponse(Call<List<TravelSpot>> call, Response<List<TravelSpot>> response) {
if (response.isSu***essful()) {
updateRecyclerView(response.body());
}
}
@Override
public void onFailure(Call<List<TravelSpot>> call, Throwable t) {
showError("网络错误:" + t.getMessage());
}
});
好处多多:
- 编译期检查类型
- 易于 Mock 测试
- 支持拦截器、缓存、认证扩展
RecyclerView:高效展示大量数据
比起 ListView,RecyclerView 更灵活、性能更强。
适配器模板:
public class SpotAdapter extends RecyclerView.Adapter<SpotViewHolder> {
private List<TravelSpot> spots;
@NonNull
@Override
public SpotViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_travel_spot, parent, false);
return new SpotViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SpotViewHolder holder, int position) {
TravelSpot spot = spots.get(position);
holder.nameText.setText(spot.getName());
Glide.with(holder.imageView).load(spot.getImageUrl()).into(holder.imageView);
}
@Override
public int getItemCount() {
return spots.size();
}
static class SpotViewHolder extends RecyclerView.ViewHolder {
TextView nameText, descText;
ImageView imageView;
SpotViewHolder(View itemView) {
super(itemView);
nameText = itemView.findViewById(R.id.text_name);
descText = itemView.findViewById(R.id.text_desc);
imageView = itemView.findViewById(R.id.image_spot);
}
}
}
优化建议:
- 使用
DiffUtil增量更新 - 图片加载用 Glide/Picasso 懒加载
- 局部刷新代替
notifyDataSetChanged() - 配合
SwipeRefreshLayout实现下拉刷新
graph TD
A[网络请求] --> B{数据是否为空?}
B -->|是| C[显示空视图]
B -->|否| D[设置Adapter]
D --> E[启用DiffUtil增量更新]
E --> F[图片懒加载(Glide)]
F --> G[局部刷新而非notifyDataSetChanged()]
这套组合拳下来,列表丝般顺滑~
整个安卓开发体系就像一座大厦,而你正在亲手一块砖一块砖地把它建起来。环境是地基,UI是外墙,网络是水电管线,生命周期是承重墙……每一部分都不可或缺。
希望这篇“去AI味”的实战笔记,能帮你少走弯路,更快成长为一名真正的 Android 工程师!🌟
Keep coding, keep exploring! 🚀
本文还有配套的精品资源,点击获取
简介:MyApplication是一款基于安卓平台的手机应用程序,采用Java语言开发,专注于提供旅游指南服务。该应用集成了地图导航、景点信息、文化介绍、餐饮住宿推荐等功能,利用Android SDK中的Google Maps API实现定位与路线规划,并通过网络API或数据库获取实时旅游数据。应用界面使用XML布局设计,结合Button、TextView等控件实现用户交互,通过Activity与Intent机制完成页面跳转和数据传递。项目以APK格式打包发布,源码结构清晰,包含在“MyApplication-master”目录中,适用于学习安卓应用开发全流程,涵盖UI设计、功能实现、资源管理与版本控制等内容。
本文还有配套的精品资源,点击获取