
简介面向移动应用开发初学者的Android Studio实战案例完整实现了QQ风格注册、登录与好友列表三个页面的相互跳转与应用内数据传递。注册界面接收用户输入的账号密码注册成功后自动将凭据传入登录界面登录时再度校验并携带用户名跳转至好友列表页好友列表不仅展示头像还呈现了消息内容细节完整。资源共544个文件包含XML布局、Java业务代码、JSON配置、Gradle构建文件以及PNG/JPG图片等Android工程常见类型压缩包约12.32MB内部既有工程源码也包含可直接安装运行的APK。已有6437人学习下载。项目注释详细适合用于课程设计或大作业参考尤其能为初学者提供Activity跳转、Intent传值、列表展示及界面复用的直观范例帮助快速搭建具备完整业务流程的社交类应用界面框架。1. 为什么一个跳转案例能让大作业少加三天班很多人在写 Android 第一个大作业时往往不是卡在业务逻辑而是卡在页面跳转和数据传递上注册完怎么把账号带给登录页、登录成功后怎么让好友列表知道当前用户是谁、返回键按下去会不会把不该出现的数据重新抛出来。这套 QQ 风格案例的价值在于它把一个完整的三层页面链路注册 → 登录 → 好友列表用最简单的原生控件实现了一遍没有引入第三方网络库也没有复杂的架构全部依赖 Android 自身的 Intent 机制和 Activity 栈行为。适合正在做课程设计、或者刚学完四大组件想找一个可运行模板的人同时对写过业务项目的人而言也能从中看到页面间传值在真实场景下的边界问题。我拆这套资源时重点看了它的传参方式、列表适配和生命周期处理下面逐块说。2. 页面布局设计与控件选型2.1 注册与登录页面的布局策略注册页和登录页从 UI 结构上非常相似都是一个账号输入框、一个密码输入框、一个操作按钮。这个案例没有用复杂的约束布局而是采用了垂直方向的 LinearLayout 配合权重分配的方式。这种做法的好处是兼容性好不需要额外引入 ConstraintLayout 依赖也避免了部分老旧设备上布局解析报错的问题。LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding24dp EditText android:idid/et_username android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请输入账号 android:maxLines1 android:inputTypetext / EditText android:idid/et_password android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop12dp android:hint请输入密码 android:maxLines1 android:inputTypetextPassword / Button android:idid/btn_register android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop24dp android:text注册 / /LinearLayout这里有个细节值得说一下密码输入框的android:inputTypetextPassword不只是让内容显示为圆点它还会影响软键盘的按键布局在真机上测试时键盘会隐藏回车键这对表单类页面来说是个容易被忽略的小坑。登录页的布局结构和注册页几乎一样只是按钮的id和文案不同很多初学者直接复制粘贴这没问题但不建议 — 因为两个页面的按钮如果id相同在多次跳转时容易捡到错误页面的控件引用后面排错时很难查。2.2 好友列表页面的 ListView 列表结构好友列表页是这个案例里最花心思的部分它没有用 RecyclerView而是选用了 ListView。从现在的开发习惯看这有点复古但对大作业场景来说ListView 的适配器模型更直观而且不依赖 RecyclerView 的 LayoutManager 概念代码量更少。每个列表项是一个自定义的 item 布局里面包含一个圆形头像、一个昵称、一条消息摘要。LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding10dp android:gravitycenter_vertical ImageView android:idid/iv_avatar android:layout_width50dp android:layout_height50dp android:srcmipmap/ic_launcher android:contentDescription好友头像 / LinearLayout android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:orientationvertical android:layout_marginStart12dp TextView android:idid/tv_nickname android:layout_widthwrap_content android:layout_heightwrap_content android:textSize16sp android:textStylebold / TextView android:idid/tv_last_message android:layout_widthwrap_content android:layout_heightwrap_content android:textSize14sp android:ellipsizeend android:maxLines1 android:textColor#999999 / /LinearLayout /LinearLayoutitem 布局里用了一个layout_weight1的中间层把右侧区域撑满让昵称和消息摘要靠左显示这种「外层横向 LinearLayout 内层垂直 LinearLayout」的组合是列表项最常见的写法。头像直接用mipmap/ic_launcher是省事的选择如果你希望视觉上更接近真实 QQ 效果可以在res/drawable下放一个圆形裁剪的 shape 文件但要注意 shape 的corners属性只能对背景生效对src图片无效需要通过BitmapShader或者第三方库裁剪这里就不展开讲了。2.3 布局文件与 Activity 的绑定关系每个布局文件通过setContentView(R.layout.activity_xxx)绑定到对应的 Activity。这里有一个新手容易踩的坑因为三个页面结构相似R 文件里有很多 id 容易混淆et_username在注册页和登录页都存在但你必须在各自的 Activity 里用findViewById找到并初始化。如果你在注册页的 Activity 里调用了登录页布局中的 id应用会直接抛空指针崩溃。所以我的习惯是先写好三个布局文件并梳理出一个 id 对照表再写 Java 代码。Activity布局文件核心控件RegisterActivityactivity_register.xmlet_username, et_password, btn_registerLoginActivityactivity_login.xmlet_username, et_password, btn_loginFriendsActivityactivity_friends.xmllv_friends, tv_current_user这个对照表写清楚之后基本可以避免「控件引用错误导致闪退」这类问题。3. Intent 页面跳转与跨页面数据传递3.1 显式 Intent 实现三步跳转页面跳转的逻辑是注册页点击注册按钮 → 携带账号密码跳转到登录页 → 登录页点击登录按钮 → 携带用户名跳转到好友列表页。这整条链路都是通过显式 Intent 完成的也就是直接指定目标 Activity 的全限定类名。为什么不建议用隐式 Intent因为隐式 Intent 需要配置 intent-filter而且系统可能弹出多个匹配应用或者找不到可处理的组件对课堂案例来说是过度设计。// RegisterActivity.java Button btnRegister findViewById(R.id.btn_register); btnRegister.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { String username etUsername.getText().toString().trim(); String password etPassword.getText().toString().trim(); if (username.isEmpty() || password.isEmpty()) { Toast.makeText(RegisterActivity.this, 账号和密码不能为空, Toast.LENGTH_SHORT).show(); return; } Intent intent new Intent(RegisterActivity.this, LoginActivity.class); intent.putExtra(username, username); intent.putExtra(password, password); startActivity(intent); } });这段代码的核心是putExtra它把账号和密码以键值对的形式写入 Intent 的附加信息中。目标 Activity 通过getIntent().getStringExtra(username)取出值。有一个细节经常被忽略trim()之后判断空值这样用户如果只输入了空格不会被认为是有效的注册信息。另外startActivity时如果当前页面还在编辑框聚焦状态跳转后键盘一般会自动收回这是系统行为不用额外处理。3.2 登录页接收数据并二次传递登录页的职责是接收注册页传来的账号密码校验后将用户名继续往下传。这里的传值逻辑是逐级透传注册页 → 登录页 → 好友列表页。正常业务逻辑中密码不应该继续往下传但作为初学者案例保留密码可以让你更直观地看到 Intent 携带数据的生命周期 — 它只存在于这次跳转的临时状态中Activity 被销毁后数据也就随之消失。// LoginActivity.java Intent intent getIntent(); String username intent.getStringExtra(username); String password intent.getStringExtra(password); etUsername.setText(username); etPassword.setText(password); Button btnLogin findViewById(R.id.btn_login); btnLogin.setOnClickListener(new View.OnClickListener() { Override public void onClick(View v) { String inputUser etUsername.getText().toString().trim(); String inputPwd etPassword.getText().toString().trim(); if (inputUser.equals(username) inputPwd.equals(password)) { Intent toFriends new Intent(LoginActivity.this, FriendsActivity.class); toFriends.putExtra(current_user, inputUser); startActivity(toFriends); } else { Toast.makeText(LoginActivity.this, 账号或密码错误, Toast.LENGTH_SHORT).show(); } } });这里有一个值得深思的点登录页把注册页传过来的数据直接回显到了 EditText 上从用户体验上讲很方便但从安全性上讲暴露了密码。做一个最小改动就能优化密码框不写回setText(password)只回显账号。如果你想把作业做得更有亮点还可以把回显改为「账号自动填充、密码留空」并在按钮点击时与传来的密码比较。此外校验逻辑是字符串相等比较实际项目中应该做加密存储和网络校验但这里受限于纯本地案例字符串比较是可接受的教学简化。3.3 好友列表页接收用户名并展示列表数据好友列表页的逻辑分成两条线一条是显示当前登录的用户名通常放在列表页顶部的标题栏区域另一条是填充好友列表数据。两者互相独立所以代码上也是分开处理的。这段代码我建议重点看列表数据的组织方式因为它是整个案例里最关键的业务展示逻辑。// FriendsActivity.java String currentUser getIntent().getStringExtra(current_user); tvCurrentUser.setText(当前用户 currentUser); ListView lvFriends findViewById(R.id.lv_friends); String[] nickNames {小明, 小红, 小刚, 小丽}; String[] lastMessages {在吗周末出来玩, 作业写完了吗, 明天运动会别忘了, 谢谢你的帮助}; int[] avatars {R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher};数据结构上用的是四个等长数组通过下标一一对应这是 ListView 体系下最简单的「模拟数据源」方案。实际开发中一定会用ListFriend这样的实体类集合但大作业场景下用数组可以减少代码量。需要留意的是数组长度必须一致否则在getView里会抛IndexOutOfBoundsException。我一般建议用ArrayList统一管理避免等长问题修改起来也方便。这段之后就是适配器的编写了放在下一章说明。4. 好友列表的数据适配与性能边界4.1 BaseAdapter 与自定义 ViewHolder 模式ListView 的适配器是连接数据源和列表 UI 的桥梁。这个案例使用了 BaseAdapter因为它可以完全控制每一项的视图生成逻辑。BaseAdapter 有四个必须重写的方法getCount、getItem、getItemId、getView。很多人会在这四个方法里偷懒比如getItemId直接返回 0这在大多数场景下没问题但如果你后面要加列表项点击跳转详情页position 定位就会不准。我的建议是养成规范写法getCount返回数据长度getItem返回 position 对应的数据对象getItemId返回 position。public class FriendAdapter extends BaseAdapter { private Context context; private ListFriendBean dataList; public FriendAdapter(Context context, ListFriendBean dataList) { this.context context; this.dataList dataList; } Override public int getCount() { return dataList null ? 0 : dataList.size(); } Override public Object getItem(int position) { return dataList.get(position); } Override public long getItemId(int position) { return position; } Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { convertView LayoutInflater.from(context).inflate(R.layout.item_friend, parent, false); holder new ViewHolder(); holder.ivAvatar convertView.findViewById(R.id.iv_avatar); holder.tvNickname convertView.findViewById(R.id.tv_nickname); holder.tvLastMessage convertView.findViewById(R.id.tv_last_message); convertView.setTag(holder); } else { holder (ViewHolder) convertView; } FriendBean bean dataList.get(position); holder.tvNickname.setText(bean.getNickName()); holder.tvLastMessage.setText(bean.getLastMessage()); return convertView; } static class ViewHolder { ImageView ivAvatar; TextView tvNickname; TextView tvLastMessage; } }getView里的复用逻辑是 ListView 调优的核心第一层是convertView是否为空为空才走加载布局和初始化控件不为空就通过getTag取回 ViewHolder。这个机制能避免每次滚动时重新 inflate 布局、重复findViewById从而显著降低列表卡顿。这个案例的数据量只有四条所以性能差别不明显但如果你把数据源换成 100 条以上就会发现不复用的写法滚动时明显掉帧。4.2 实体类封装与数据初始化为了配合适配器我建议在代码里定义一个FriendBean实体类把昵称、最后一条消息、头像资源 id 三个字段封装起来。这样做不仅让适配器代码更整洁也符合 Java 面向对象的思路。好友列表页面的数据初始化可以直接在onCreate里同步进行因为是本地写死的数据不需要异步加载。如果数据来自网络或数据库就必须考虑延迟加载的问题这个案例里不需要。public class FriendBean { private String nickName; private String lastMessage; private int avatarResId; public FriendBean(String nickName, String lastMessage, int avatarResId) { this.nickName nickName; this.lastMessage lastMessage; this.avatarResId avatarResId; } public String getNickName() { return nickName; } public String getLastMessage() { return lastMessage; } public int getAvatarResId() { return avatarResId; } }初始化过程就是创建四个FriendBean对象存入 ArrayList然后lvFriends.setAdapter(new FriendAdapter(this, list))。注意实体类要提供 getter 方法适配器里通过 getter 读取字段不要直接访问私有属性这是 Java 封装性的要求。如果你习惯 Kotlindata class 可以自动生成这些方法但 Java 版本还是老老实实手写。4.3 ListView 点击事件的扩展思路好友列表通常有点击好友进入聊天页的需求这个案例没有实现聊天页但点击事件仍然值得加。setOnItemClickListener的回调方法里能拿到 position你可以在回调里获取对应好友的信息用 Toast 展示或者跳转到一个新的 Activity 并传递好友昵称。代码如下lvFriends.setOnItemClickListener(new AdapterView.OnItemClickListener() { Override public void onItemClick(AdapterView? parent, View view, int position, long id) { FriendBean bean dataList.get(position); Toast.makeText(FriendsActivity.this, 点击了 bean.getNickName(), Toast.LENGTH_SHORT).show(); } });这里要注意position和id的区别如果前面getItemId写的是 0点击时拿到的 id 全是 0无法用来区分条目而 position 是当前条目在列表中的下标从 0 开始。在适配器的数据结构发生变化时比如删除某一条position 会随之改变但 id 相对稳定。这也是为什么前面强调getItemId要返回 position 的原因。5. 调试排错思路与应用扩展调试页面跳转类案例时最大的难点在于追踪数据传递链是否完整。我把这套案例运行时会遇到的典型问题分成三类并给出解决思路。第一类是空指针崩溃症状是点击按钮直接闪退原因基本逃不出findViewById使用了错误的 id或者布局文件和 Activity 不匹配优先检查setContentView与布局资源是否对应再用 Logcat 定位到具体行号。第二类是数据传递为 null症状是好友列表页的标题显示「当前用户null」这是因为登录页跳转时使用了错误的 key或者putExtra的 key 与getStringExtra的 key 不一致我建议把三步跳转的 key 提取成常量统一管理。第三类是列表不显示数据症状是页面正常打开但列表空白原因是适配器没有被 setAdapter或者数据集在 setAdapter 之后没有通过notifyDataSetChanged刷新。第三类问题有一个很隐蔽的分支就是ListView嵌套在其他可滚动布局中。如果好友列表页的外层布局用的是ScrollViewListView 的高度会测量异常页面打开后只能看到第一条数据或者干脆看不到任何条目。解决方法是把 ListView 换成GridView、RecyclerView或者动态计算 ListView 高度重设LayoutParams最简单实用的路径是不用 ScrollView 包裹。实际上列表页面本身就能滚动外层再包一个 ScrollView 完全没有必要。adb logcat -s AndroidRuntime:E上面这个命令可以只过滤 Android 运行时崩溃日志适合快速定位崩溃原因。Logcat 中FATAL EXCEPTION后面的行号会精准指向 Java 代码中出错的位置打开 Android Studio 底部的 Logcat 窗口点击对应行就能跳转到源码处。进一步优化方向有两个一是把模拟数据抽离到一个单独的DataSource类中让好友列表页只负责视图展示这算是一个轻量的 MVC 分层思想二是加入 Activity 返回栈管理注册页跳转到登录页时调用finish()销毁自身这样用户登录后按返回键不会回到注册页而是直接退出应用更贴近真实软件的交互习惯。像Intent.FLAG_ACTIVITY_CLEAR_TOP这类标志位也可以配合使用但要理解它清除栈顶以上 Activity 的副作用。这样一个原本只有跳转功能的案例就能在课程设计答辩时显示出你对组件生命周期和用户交互链路的完整理解。本文还有配套的精品资源点击获取