ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android页面跳转与数据回传:仿QQ登录注册好友列表实战

Android页面跳转与数据回传:仿QQ登录注册好友列表实战 简介这是基于Android Studio开发的QQ风格社交应用入门项目适合Android初学者、移动应用开发课程设计或毕业设计参考。项目完整实现注册、登录与好友列表三个核心界面及相互跳转逻辑注册时输入的账号密码可自动回传至登录页登录后账号随页面跳转传递至好友列表流程清晰便于理解Activity间数据传递与Intent用法。资源包为zip格式共422个文件包含Java源码、XML布局、Gradle构建配置、JSON数据、图片资源及APK文件等压缩包大小11.6MB目录结构完整可直接导入Android Studio运行查看效果。好友列表界面设计细致实现在线头像与消息内容展示登录界面视觉美观整体完成度高。目前已有11704人学习下载配套博客提供详细介绍是快速上手Android界面跳转与数据传递的实用参考。1. 这个 QQ 风格工程为什么适合做页面跳转入门很多人在学完 Android 四大组件后第一个动手练习就是模仿 QQ 做一个登录、注册、好友列表的串联页面。结果往往卡在同一个地方从登录页跳到注册页容易但注册完怎么把账号密码“带回来”这个 Android Studio 工程恰好把这条链路完整走通了而且没有任何多余依赖不涉及网络、数据库、MVVM核心就是把 Activity 的显式跳转、startActivityForResult回传、ListView数据绑定这三个基础能力循环用熟。它很适合正在选安卓移动应用开发课程设计题目的读者也适合已经写过单页面应用、想补上“页面间数据流”这块短板的开发者。下面按工程实际结构从布局到跳转再到列表展示逐步拆开看。2. 拆解 Android Studio 项目结构与登录页布局2.1 三个界面对应哪些类与资源打开工程后重点看app/src/main目录。三个页面分别对应LoginActivity、RegisterActivity、FriendListActivity布局文件是activity_login.xml、activity_register.xml、activity_friends.xml好友列表的 item 单独放在item_friend.xml。典型结构如下app/src/main/java/com/example/qqdemo/ |-- LoginActivity.java |-- RegisterActivity.java |-- FriendListActivity.java |-- adapter/ | -- FriendAdapter.java app/src/main/res/layout/ |-- activity_login.xml |-- activity_register.xml |-- activity_friends.xml -- item_friend.xml包名不一定是 qqdemo拉下来的工程如果不同不影响阅读。每个 Activity 都必须在AndroidManifest.xml里注册否则一启动就崩。这个文件往往是初学阶段最容易被忽略的下面表格总结了三个页面的注册关系页面Activity 类Manifest 注册入口职责登录页LoginActivityactivity android:name.LoginActivity并带 LAUNCHER 意图过滤器输入账号密码跳转注册或进入好友列表注册页RegisterActivityactivity android:name.RegisterActivity /输入账号、密码、确认密码回传数据好友列表页FriendListActivityactivity android:name.FriendListActivity /展示好友头像、昵称、消息并显示登录账号Manifest 里的点号表示相对包名写.LoginActivity会被自动解析为完整类名。常见的翻车点是清单文件忘改android:exported在 Android 12 及以上设备安装时直接报错。如果 targetSdk 是 31 或更高带 intent-filter 的入口 Activity 必须显式声明android:exportedtrue其他不需要被外部启动的 Activity 写false即可。一个可用的入口片段如下activity android:name.LoginActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity activity android:name.RegisterActivity android:exportedfalse / activity android:name.FriendListActivity android:exportedfalse /这段配置里只有登录页是应用入口注册页和好友列表页都不允许外部启动符合这个项目的数据流向。2.2 登录页的圆角输入框与渐变按钮登录界面想要做得好看不一定需要自定义 View用 shape drawable 就能解决大部分观感问题。输入框去掉默认下划线背景换成浅色圆角矩形占位文案和左侧图标通过hint和drawableStart设置例如EditText android:idid/et_account android:layout_widthmatch_parent android:layout_height52dp android:hint账号 android:backgrounddrawable/bg_edit_text android:paddingHorizontal16dp android:drawableStartmipmap/ic_account android:drawablePadding8dp /bg_edit_text.xml是典型的 shape 资源shape xmlns:androidhttp://schemas.android.com/apk/res/android solid android:color#F2F3F5 / corners android:radius8dp / stroke android:width1dp android:color#DDDDDD / padding android:left12dp android:top8dp android:right12dp android:bottom8dp / /shape这里的corners圆角半径不要超过控件高度的三分之一否则部分 ROM 上会出现上下背景露出白色尖角。登录按钮可以用gradient做从左到右的渐变angle0表示水平方向angle90才是从上到下。按钮还需要加一点阴影或者按压色差课程设计里通常会在selector中写按下态与默认态两种背景避免点击时按钮“消失”。2.3 登录跳转的显式 Intent 写法LoginActivity 的逻辑并不复杂拿到输入内容判空然后构造显式 Intent 跳到好友列表页账号通过putExtra附加在 Intent 上。代码结构如下public class LoginActivity extends AppCompatActivity { private EditText etAccount; private EditText etPassword; private static final int REQUEST_REGISTER 1; public static final String EXTRA_ACCOUNT account; public static final String EXTRA_PASSWORD password; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); etAccount findViewById(R.id.et_account); etPassword findViewById(R.id.et_password); Button btnLogin findViewById(R.id.btn_login); Button btnRegister findViewById(R.id.btn_register); btnLogin.setOnClickListener(v - { String account etAccount.getText().toString().trim(); String password etPassword.getText().toString().trim(); if (account.isEmpty() || password.isEmpty()) { Toast.makeText(this, 账号或密码不能为空, Toast.LENGTH_SHORT).show(); return; } Intent intent new Intent(LoginActivity.this, FriendListActivity.class); intent.putExtra(EXTRA_ACCOUNT, account); startActivity(intent); }); btnRegister.setOnClickListener(v - { Intent intent new Intent(LoginActivity.this, RegisterActivity.class); startActivityForResult(intent, REQUEST_REGISTER); }); } }trim()去掉首尾空格防止用户手滑多打一个空格导致后面比对账号时不一致。new Intent(LoginActivity.this, FriendListActivity.class)是显式 Intent直接指定目标 Activity不需要系统去匹配 action 和 category。startActivity(intent)用于普通跳转而跳转注册页时用的是startActivityForResult因为登录页要拿注册结果这对应下一章的数据回传链路。3. 注册页回传数据setResult 与 onActivityResult 的完整配合3.1 注册页的“注册成功”只是回传数据这个项目的注册页并不把账号写入数据库它只是把输入内容通过Intent带回登录页。理解这一点代码就能看得通注册按钮点击后构建一个空 Intent把账号密码放进去再调用setResult把 Intent 返回给启动方最后finish()销毁自己。public class RegisterActivity extends AppCompatActivity { private EditText etAccount; private EditText etPassword; private EditText etConfirm; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_register); etAccount findViewById(R.id.et_register_account); etPassword findViewById(R.id.et_register_password); etConfirm findViewById(R.id.et_register_confirm); findViewById(R.id.btn_register_submit).setOnClickListener(v - { String account etAccount.getText().toString().trim(); String password etPassword.getText().toString().trim(); String confirm etConfirm.getText().toString().trim(); if (account.isEmpty() || password.isEmpty()) { Toast.makeText(this, 账号和密码不能为空, Toast.LENGTH_SHORT).show(); return; } if (!password.equals(confirm)) { Toast.makeText(this, 两次输入的密码不一致, Toast.LENGTH_SHORT).show(); return; } Intent resultIntent new Intent(); resultIntent.putExtra(LoginActivity.EXTRA_ACCOUNT, account); resultIntent.putExtra(LoginActivity.EXTRA_PASSWORD, password); setResult(RESULT_OK, resultIntent); finish(); }); } }注意setResult必须发生在finish()之前否则 Activity 退出时系统拿不到结果登录页会收到RESULT_CANCELED。这里的resultIntent不需要设置 Class因为它不是用来启动页面而是作为结果数据的载体由系统原样交给LoginActivity。两个常量直接复用LoginActivity里定义的静态字段避免魔法字符串散落各处。3.2 onActivityResult 的三参数判定登录页接收注册结果的入口是onActivityResult重写时需要同时判断请求码、结果码和返回数据缺一个都可能产生误判Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode REQUEST_REGISTER resultCode RESULT_OK data ! null) { String account data.getStringExtra(EXTRA_ACCOUNT); String password data.getStringExtra(EXTRA_PASSWORD); etAccount.setText(account); etPassword.setText(password); } }三个参数的作用可以记成一张小表参数含义典型值不判断的后果requestCode区分这次跳转从哪里来自定义整数比如 1多个页面共用回调时互相覆盖resultCode返回方是否处理成功RESULT_OK / RESULT_CANCELED用户按返回键也被当成注册成功data返回方携带的数据Intent 内部包含 extra直接 getStringExtra 得到 null这里最容易踩的坑是用户没有注册直接按返回键此时resultCode是RESULT_CANCELEDdata也为 null三判都没有通过界面不会做任何事。如果把判空省略data.getStringExtra会抛空指针。课程设计验收时老师经常故意按返回键这一步是加分点。3.3 为什么不用全局变量传递注册数据很多初学者会把账号密码存进一个静态变量两个 Activity 直接读写看起来更简单。但全局变量的生命周期不受 Activity 控制登录页被系统回收后重新创建时静态变量可能还在而界面状态却丢失了容易出现脏数据。Activity 之间的数据传递使用 Intent 是 Android 官方推荐的方式因为 Intent 数据会随任务栈绑定Activity 被回收重建后也能从savedInstanceState恢复。这个课程设计项目里没有持久化需求用 setResult 回传最合适。如果将来要保存真实注册用户再引入数据库或 SharedPreferences但不建议在入门项目里混用。4. 好友列表页ListView 适配器与账号传递4.1 登录账号如何被带到好友列表页登录按钮跳转好友列表页时通过intent.putExtra(EXTRA_ACCOUNT, account)把账号放进了 Intent。FriendListActivity只需要在onCreate里取出来展示在标题栏或者欢迎语中。注意账号可能为空要提供默认值public class FriendListActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_friends); String account getIntent().getStringExtra(LoginActivity.EXTRA_ACCOUNT); if (account null) { account 游客; } TextView tvTitle findViewById(R.id.tv_title); tvTitle.setText(好友列表 account ); ListView listView findViewById(R.id.lv_friends); ListFriend friends buildFriends(); FriendAdapter adapter new FriendAdapter(this, friends); listView.setAdapter(adapter); } private ListFriend buildFriends() { ListFriend friends new ArrayList(); friends.add(new Friend(小刚, 周末打球吗, R.drawable.bg_avatar_green)); friends.add(new Friend(阿伟, 作业写完没, R.drawable.bg_avatar_blue)); return friends; } }savedInstanceState和getIntent不要搞混savedInstanceState是 Activity 被销毁重建后恢复自己的状态getIntent是获取启动这个 Activity 的那个 Intent两者都可以携带数据但上下文完全不同。账号展示在标题栏这种设计能很直观地验证数据确实从登录页传到了列表页。4.2 好友数据模型与静态数据填充好友列表每个 item 包含头像、昵称、消息三块信息所以先定义一个 Friend 实体类。这里不引入 Room 或 Realm只写一个带三个成员变量的普通类public class Friend { private String name; private String message; private int avatarResId; public Friend(String name, String message, int avatarResId) { this.name name; this.message message; this.avatarResId avatarResId; } public String getName() { return name; } public String getMessage() { return message; } public int getAvatarResId() { return avatarResId; } }头像用 drawable 形状占位比如不同颜色的圆形背景。这样避免引入图片加载框架同时也够课程设计展示。如果以后要换成真实头像只需要把avatarResId改成图片资源 id或者用 Glide 等框架把字段类型改成 URL 字符串。消息内容可以写死也可以配合随机文案生成但这里的重点是列表绑定不推荐在入门项目里过早加入网络请求。4.3 BaseAdapter 的四个关键方法与 ViewHolder 复用ListView 无法直接接收ListFriend需要继承 BaseAdapter 做一个适配器把数据变成 View。这个项目的 FriendAdapter 核心代码public class FriendAdapter extends BaseAdapter { private final ListFriend friends; private final LayoutInflater inflater; public FriendAdapter(Context context, ListFriend friends) { this.friends friends; this.inflater LayoutInflater.from(context); } Override public int getCount() { return friends.size(); } Override public Object getItem(int position) { return friends.get(position); } Override public long getItemId(int position) { return position; } Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView null) { convertView inflater.inflate(R.layout.item_friend, parent, false); } TextView tvName convertView.findViewById(R.id.tv_friend_name); TextView tvMessage convertView.findViewById(R.id.tv_friend_message); ImageView ivAvatar convertView.findViewById(R.id.iv_friend_avatar); Friend friend friends.get(position); tvName.setText(friend.getName()); tvMessage.setText(friend.getMessage()); ivAvatar.setImageResource(friend.getAvatarResId()); return convertView; } }BaseAdapter 方法虽多核心就这四类方法返回值作用getCount列表项数量ListView 计算可滚动区域高度getItemposition 对应的数据对象点击、删除时定位数据getItemId当前项 id一般直接返回 positiongetView当前位置的 View每一项滑入屏幕时调用getView里做了 convertView 判空这是 ListView 性能的关键。滚动时滑出屏幕的 item 会被缓存convertView 不为 null 时直接复用避免每次都 inflate 导致卡顿。课程设计的列表通常只有几行不明显但面试时问“ListView 为什么要用 ViewHolder 复用”指的就是这个位置。如果 item 里有独立点击事件建议在getView里设置监听而不是重写onItemClick后再判断这样事件和数据绑定更直观。5. 把 startActivityForResult 升级为 ActivityResult API 并验证生命周期5.1 旧 API 的兼容坑与新写法startActivityForResult和onActivityResult从 AndroidX Activity 1.2.0 开始标记为弃用但课程设计项目里依然能编译运行。新项目建议改用ActivityResultLauncher代码更安全也更好读。登录页声明一个注册启动器private final ActivityResultLauncherIntent registerLauncher registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result - { if (result.getResultCode() RESULT_OK result.getData() ! null) { String account result.getData().getStringExtra(EXTRA_ACCOUNT); String password result.getData().getStringExtra(EXTRA_PASSWORD); etAccount.setText(account); etPassword.setText(password); } });触发跳转时调用registerLauncher.launch(intent)即可不需要再写onActivityResult。注意registerForActivityResult必须在 Activity 进入 STARTED 状态前完成注册放在 onCreate 里或者字段声明位置都可以。如果放在按钮点击后才调用会抛出LifecycleOwner is attempting to register while current state is RESUMED。5.2 用生命周期日志验证整个跳转链路调试这种多页面数据流时界面表现不够定位问题。建议在每个 Activity 里覆盖onStart、onResume、onPause、onStop打印类名日志Override protected void onStart() { super.onStart(); Log.d(LifecycleTrace, getClass().getSimpleName() onStart); } Override protected void onResume() { super.onResume(); Log.d(LifecycleTrace, getClass().getSimpleName() onResume); } Override protected void onPause() { super.onPause(); Log.d(LifecycleTrace, getClass().getSimpleName() onPause); } Override protected void onStop() { super.onStop(); Log.d(LifecycleTrace, getClass().getSimpleName() onStop); }在 Logcat 里按LifecycleTrace过滤跑一遍“登录页 → 注册页 → 按注册按钮 → 回到登录页”的流程正常顺序应该是LoginActivity onPause RegisterActivity onStart RegisterActivity onResume RegisterActivity onPause LoginActivity onActivityResult RegisterActivity onStop RegisterActivity onDestroy LoginActivity onResume如果日志里LoginActivity onActivityResult没有出现优先级先查 requestCode 和 resultCode 是否匹配如果出现了但界面上没有回填账号再查注册页是否先 setResult 再 finish。把按钮点击日志、setResult 调用日志和生命周期日志一起打开基本能精确定位到是哪一环丢数据这种排错思路比盯着界面一行行猜要快很多。本文还有配套的精品资源点击获取
返回列表