Android模拟手机QQ登录界面和主界面(含源码),这类项目在Android学习圈里其实一直很火。我见过不少初学者把自己的第一个完整UI项目就定在这个方向上,也有不少人在面试前临时抱佛脚拿它练手。原因很简单:登录界面加主界面,几乎涵盖了Android UI开发最核心的几块内容——布局嵌套、输入控件定制、页面跳转、数据传递、状态切换。把一个仿QQ项目从头到尾做一遍,你对Android的界面开发体系基本就有感觉了。这篇文章不打算给你贴一大坨源码了事,而是把整个项目从设计思路到坑位排查完整拆开讲,你可以直接照着思路重构一遍,也可以拿它作为自己项目的底子。
1. 项目整体设计与思路拆解
1.1 为什么选“QQ登录+主界面”作为练手项目
先说结论:这个项目几乎是为Android UI入门到进阶量身定做的。登录界面看起来简单,实际上隐含的知识点一点都不少。
最外层是一个竖向排列的容器布局,内部包含了Logo区域、账号输入框、密码输入框、登录按钮、注册入口,还有“同意用户协议”这种辅助性文案。这里面的每个元素都对应一个Android基础知识点:输入框要处理EditText的焦点切换、密码的明文密文切换;登录按钮要处理按压态、不可用态的UI变化;页面底部还涉及软键盘弹出时的避让策略。等你把这些细节全部处理好,你的布局基本功基本就过关了。
主界面就更典型了。QQ的主界面是经典的底部导航栏加多个页面切换结构,对应到Android里就是用Fragment加上ViewPager2、BottomNavigationView或者TabLayout来实现。里面还会涉及列表页、消息页、联系人页之间数据的切换与状态保持。把这个部分做完,你对于“多页面应用如何组织”会有非常具体的理解。
更关键的是,这两个界面整合在一起后,你能自然接触到Android开发中绕不开的一整套流程:Activity跳转、Intent传参、生命周期切换、数据持久化(比如记住账号密码)、屏幕适配、状态栏颜色处理等等。这些知识点如果分开学,总觉得是散的,但落在这样一个项目里,全都有了一个具体的应用场景。
1.2 整体技术选型与架构演进思路
先说清楚一个前提:这个项目的定位是“界面模拟”,不是真正去对接QQ的后台协议。所以它的技术选型完全可以用轻量方案,不需要引入重型网络框架,也不需要做消息推送。
我自己的建议分三个阶段走,对应不同基础的人:
- 入门版:只用一个
Activity承载登录界面,再用一个Activity承载主界面,登录成功后通过Intent直接跳转。数据传递用最基础的putExtra,账号密码记忆用SharedPreferences。这套方案的好处是代码量小,逻辑链路清晰,非常适合第一次接触Android的开发者。 - 进阶版:把主界面里面的多个页面抽成
Fragment,用BottomNavigationView加Fragment切换来模拟QQ的底部导航;登录状态用对象形式的Session保存,而不是每次都读SharedPreferences。 - 工程化版本:引入
ViewModel加LiveData管理登录状态,用Navigation组件管理页面跳转,甚至可以把登录模块和主界面模块拆成不同的包,模拟真实项目里的模块划分。
我见过不少新手一上来就上Jetpack Compose,实际上对于这个项目,传统的View体系仍然是更好的学习选择。原因在于Compose虽然新,但很多公司现有的存量代码还是传统View体系,而且传统View体系里layout_weight、Selector、自定义Drawable这些概念能让你更直观地理解Android的UI渲染流程。
1.3 项目目录结构与职责划分
不管你是自己从头写还是参考现成源码,我强烈建议先想清楚目录结构,再来写代码。仿QQ界面这个项目虽然体量不大,但目录清晰与否直接影响你后面扩展功能的体验。
我习惯的结构是这样的:
app/src/main/java/com/example/demoqq/ ├── activity/ // 存放LoginActivity、MainActivity ├── fragment/ // 存放主界面的各个Fragment(消息、联系人、动态等) ├── adapter/ // Fragment适配器、列表适配器 ├── model/ // 用户信息、消息数据模型 ├── utils/ // 状态栏工具、屏幕适配工具 └── view/ // 自定义View(比如仿QQ的删除按钮、气泡等)资源文件方面,layout目录里放登录页和主界面的XML布局,drawable目录里放各种圆角背景、按钮Selector,mipmap里放应用图标。把界面元素和逻辑代码分开,后期改样式的时候不用动Java/Kotlin代码,这是Android开发里最基本的一条工程原则。
我看过太多把几十个XML布局全堆在一个文件夹里、靠命名区分用途的项目。在小项目里还能忍,一旦页面多了,找一个activity_main.xml都要翻半天。这个仿QQ项目正好是养成好习惯的最佳机会,毕竟登录页和主界面恰好是两个逻辑上完全独立的模块,天然适合分离。
2. 核心细节解析与实操要点
2.1 登录界面布局的拆解与实现
登录界面的布局从视觉上可以拆成四个区域:顶部Logo区、中间表单区、底部操作区、隐藏的协议区。初看简单,但实操中要踩的坑主要在“输入框样式”和“键盘避让”上。
先看表单区的根布局。我建议用LinearLayout配合layout_weight来分配屏幕空间,而不是用绝对定位或者FrameLayout硬怼。具体来说,Logo区占一份权重,表单区占一份权重,底部操作区固定在底部,这样在不同屏幕尺寸下都能自动适配。
账号输入框和密码输入框的处理是重中之重。QQ登录框的视觉特点是:下划线分隔、圆角浅色背景、聚焦时边框变色。实现方案不复杂,核心是用EditText配合Drawable的Selector。你需要定义三种状态:默认状态、聚焦状态、不可用状态。比如默认状态下的背景是一个灰色圆角矩形,聚焦状态变成蓝色圆角矩形加细边框,这样用户一聚焦输入框就能获得视觉反馈。
XML里伪代码大概是这个意思:
<EditText android:id="@+id/et_account" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请输入账号" android:background="@drawable/edittext_bg_selector" android:padding="12dp" android:textSize="14sp" android:inputType="text"/>然后edittext_bg_selector.xml里用selector指定两种状态的背景:
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_focused="true"> <shape android:shape="rectangle"> <solid android:color="@android:color/white"/> <stroke android:width="1dp" android:color="#4C8DFF"/> <corners android:radius="6dp"/> </shape> </item> <item> <shape android:shape="rectangle"> <solid android:color="#F5F6F7"/> <stroke android:width="1dp" android:color="#DDDDDD"/> <corners android:radius="6dp"/> </shape> </item> </selector>密码框要做明文密文切换,这需要你在布局里放一个切换按钮(通常是一个眼睛图标)。纯用XML做不到,必须写一点逻辑代码。我比较推荐的做法是用TextInputLayout包裹EditText,然后开启app:passwordToggleEnabled="true"。这个类封装的逻辑已经很完善了,包括切换动画和内容清除。如果你希望完全仿照QQ的交互体验,可以自定义一个EditText,在右侧绘制眼睛图标,并监听onTouchEvent切换TransformationMethod。
QQ输入框还有一个容易被忽略的细节:清除按钮。当输入框有内容时,右侧会显示一个“×”号,点击后清空内容。这个功能用EditText的addTextChangedListener加CompoundDrawable就能实现。代码逻辑如下:
editText.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { if (s.length() > 0) { editText.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_clear, 0); } else { editText.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } } // 省略其他回调 });然后拦截右侧图标的点击事件,调用setText("")。注意Drawable的边界要设置得比图标实际尺寸大一些,否则点击区域过小,用户经常点了没反应。
2.2 登录按钮的状态管理与交互反馈
登录按钮在仿QQ项目里的状态管理是很多人忽略的部分。QQ的登录按钮有非常明确的交互逻辑:账号和密码都不为空时,按钮是亮蓝色可点击状态;有一个为空或者都为空时,按钮是灰色的禁用状态。
实现方式有两种。一种是监听两个EditText的TextWatcher,每次都判断当前输入状态,然后调用setEnabled(true/false)。这种方式代码直接,但需要在两个输入框上分别注册监听,逻辑稍显冗余。第二种是引入一个简单的LoginViewModel,把输入框绑定到LiveData上,然后用MediatorLiveData合并两个输入流,根据合并结果自动更新按钮状态。这种方式代码量更大,但逻辑更清晰,而且为后续的登录请求做了状态管理铺垫。
我自己更推荐第一种,理由是这个项目的定位是UI模拟,核心是让新手看得懂。第二种的抽象程度对初学者来说偏高,容易把注意力从UI细节转移到架构细节上。
按钮按下后的反馈也很关键。QQ的登录按钮点击后会有一个“正在登录”的态,这时候按钮文字变成“登录中...”,并且按钮本身不可再次点击。这个不要用Dialog去实现,直接在按钮上做状态切换就行。你可以在LoginActivity里定义一个loginState变量,用Boolean标识当前是否正在登录,在登录方法入口处置为true并更新UI,在网络请求回调之后置回false。虽然这个项目没有真正的网络请求,但用Handler.postDelayed模拟一下网络延迟非常必要,能让你把用户可感知的交互状态调教到位。
提示:务必处理快速重复点击的问题。即使你设置了
setEnabled(false),在某些极端情况下点击事件仍然可能穿透。更稳妥的做法是在onClick回调里再判断一次loginState,双重保险。
2.3 主界面的结构组织与底部导航实现
主界面是整个项目里包含页面最多、最容易写乱的部分。QQ主界面底部有四个Tab:消息、联系人、动态、我的。我建议你用ViewPager2加TabLayout来实现这四个页面的切换,而不是用老掉牙的FragmentTransaction手动切换。
为什么推荐ViewPager2?因为它天然支持页面滑动切换,配合TabLayout之后,指示器和页面联动是自动处理的,代码量少很多。传统的手动切换Fragment,你需要自己管理show、hide,还要防止重建后的状态丢失,复杂度明显更高。
实现的关键步骤是这样的:
第一,新建四个Fragment类,分别对应四个Tab。每个Fragment的布局可以先放一个简单的TextView占位,后续再填充真实的列表。
第二,创建一个FragmentStateAdapter子类。这个适配器需要返回四个Fragment实例,并正确实现getItemCount。要注意,ViewPager2要求适配器必须是无参数构造的Fragment工厂方式,如果你用了带参数的构造方法,会直接崩。
public class MainPagerAdapter extends FragmentStateAdapter { public MainPagerAdapter(FragmentActivity fa) { super(fa); } @Override public Fragment createFragment(int position) { switch (position) { case 0: return new MessageFragment(); case 1: return new ContactFragment(); case 2: return new MomentFragment(); case 3: return new ProfileFragment(); default: return new MessageFragment(); } } @Override public int getItemCount() { return 4; } }第三,在MainActivity的布局里放TabLayout和ViewPager2,然后在onCreate里把它们关联起来:
TabLayout tabLayout = findViewById(R.id.tab_layout); ViewPager2 viewPager = findViewById(R.id.view_pager); viewPager.setAdapter(new MainPagerAdapter(this)); // 使用TabLayoutMediator建立关联 TabLayoutMediator mediator = new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> tab.setText(tabTitles[position])); mediator.attach();注意TabLayoutMediator必须调用attach(),否则Tab和数据源不会关联,这是最容易漏的一步。
如果你想把图标也加进去,就在TabLayoutMediator的回调里,通过tab.setIcon()设置图标资源。这里有个坑:图标需要准备选中和未选中两套状态,用Selector来选择对应状态下的图片,否则图标颜色不会随选中状态变化。
2.4 消息列表页的细节实现
如果你想让主界面看起来更像一回事,消息页不能只放一个空白Fragment。用RecyclerView展示一个简单的消息列表,是这类项目最标配的下一步。
列表项布局仿照QQ的简单形态:左边一个圆形头像,中间是名称和最后一条消息预览,右边是时间。这个布局用RelativeLayout或者ConstraintLayout都行,我习惯用ConstraintLayout,因为约束关系更直观。
头像的圆形效果是这里一个容易卡住的点。最简单的方式是用CircleImageView这个第三方库,一行代码搞定圆形裁剪。但既然是练手项目,我更推荐自己用BitmapShader实现一次圆形裁剪,核心代码二三十行,能让你理解Canvas绘制的基本逻辑。
简单说明一下思路:拿到Bitmap之后,创建一个与View宽高一致的正方形画布,把BitmapShader设置给Paint,然后画一个圆形。关键是BitmapShader的TileMode要设置为CLAMP,否则头像的边缘会出现拉伸。这一步做完,你对自定义绘制的理解会提升一个台阶。
消息列表的数据可以写死在本地。用一个List<Message>对象,里面包含name、avatarResId、lastMsg、time字段,然后交给Adapter渲染。为了让列表有一点真实感,可以提前造十几条不同好友的模拟数据,比如“文件传输助手”“产品群”“技术交流群”等。但要避免真实人物名。
列表的Adapter建议直接继承RecyclerView.Adapter,不要偷懒用ArrayAdapter。RecyclerView是现在Android列表开发的主流方案,ViewHolder的模式也是标配,早一天熟悉早一天受益。
3. 实操过程与核心环节实现
3.1 环境准备与工程创建
动手之前先把环境理清楚。我用的是Android Studio Hedgehog版本,Gradle用8.2以上,compileSdk34,minSdk24,targetSdk34。minSdk选24是因为Android 7.0的覆盖率高,而且很多新API不用做兼容判断。
创建工程时直接选Empty Views Activity,语言建议先选Java。不是Kotlin不好,而是仿QQ这个项目的很多开源样例和参考资料都是Java写的,你遇到问题搜答案的时候,Java版本的资料命中率明显更高。等把项目整体跑通了,再迁移到Kotlin会顺畅很多。
工程创建完了,先别急着写代码。把项目跑起来,在模拟器上确认空工程能正常启动,再开始动手。这个步骤能帮你把“环境问题”和“代码问题”隔离开,后面排查问题的时候少受干扰。
3.2 登录页UI的落地实践
登录页布局我建议用ConstraintLayout做根布局,比LinearLayout的嵌套层级更少,渲染性能更好。不过为了让布局结构清晰,内部嵌套一层LinearLayout完全没问题,Android的布局层级并没有那么恐怖,别过度优化。
从视觉上自上而下排列:顶部空出约80dp的间距,然后是Logo图片,用ImageView加载;紧接着是账号输入框,下方是密码输入框;再往下是登录按钮,宽度占满屏幕但左右留16dp边距;最后是底部的“注册账号”入口。
有一个细节值得提:输入框之间的间距不要用margin硬写,而是用Space控件或者include分割。这样后续调整间距时,只需要改一个地方,不用来回改多个控件的margin值。
Logo图片建议用矢量图,也就是VectorDrawable格式。矢量图在不同屏幕密度下不会失真,而且文件体积比PNG小很多。如果你手头没有合适的QQ Logo素材,可以先放一个占位的圆角正方形,中间写“QQ”,重点是练习布局逻辑。
3.3 登录校验与页面跳转的代码实现
布局完成之后,接下来需要把登录动作真正串起来。在LoginActivity里,给登录按钮设置OnClickListener,点击时读取两个输入框的文本,做基础的非空校验。
这里要注意一个习惯:在读取EditText文本时,先通过TextUtils.isEmpty()判断,再使用trim()去掉首尾空格。QQ的登录框其实不允许账号带空格,你可以在TextWatcher里直接拦截空格输入,把空格自动去掉,体验更真实。
校验通过后,进入“模拟登录”流程。因为没有真实的后端接口,我用一个延迟任务模拟:
private void doLogin(String account, String password) { loginState = true; btnLogin.setText("登录中..."); btnLogin.setEnabled(false); new Handler(getMainLooper()).postDelayed(() -> { loginState = false; btnLogin.setText("登录"); btnLogin.setEnabled(true); // 模拟登录成功,跳转主界面 Intent intent = new Intent(LoginActivity.this, MainActivity.class); intent.putExtra("account", account); startActivity(intent); finish(); }, 1500); }这个延迟设置成1.5秒,是为了让你能看到按钮状态的变化过程。延迟太短,按钮的“登录中”状态一闪而过,看不出效果;延迟太长,用户体验又很假。
跳转之后记得调用finish()。这样用户从主界面按返回键时,不会回到登录页,而是直接退出应用。这是真实App的常规做法,也是模拟登录过程必须养成的习惯。
3.4 登录状态保持与自动登录
真实QQ有“记住密码”和“自动登录”选项。虽然这是一个模拟项目,这个功能值得加上,因为它能让你练习SharedPreferences的使用。
在登录成功时,把账号和密码写入SharedPreferences。如果用户勾选了“自动登录”,再额外保存一个autoLogin标记位:
SharedPreferences.Editor editor = getSharedPreferences("login_pref", MODE_PRIVATE).edit(); if (cbAutoLogin.isChecked()) { editor.putString("account", account); editor.putString("password", password); editor.putBoolean("auto_login", true); } else { editor.clear(); } editor.apply();apply()和commit()的区别要讲一下。apply()是异步写入,不会阻塞主线程;commit()是同步写入,返回值能告诉你写入是否成功。在UI线程里建议用apply(),除非你需要关注写入结果。
然后在登录页的onCreate里做自动登录检查:
SharedPreferences prefs = getSharedPreferences("login_pref", MODE_PRIVATE); if (prefs.getBoolean("auto_login", false)) { String account = prefs.getString("account", ""); String password = prefs.getString("password", ""); doLogin(account, password); }这个功能看似简单,但它会引出一个问题:自动登录的时候,界面可能还没完全渲染出来,按钮点击会有视觉跳跃。解决方案是在doLogin里只负责逻辑跳转,不依赖按钮引用,这样不管是手动点击还是自动登录,走的是同一条路径。
3.5 主界面的搭建与Fragment页面组织
主界面的代码组织直接决定这个项目后续是否好扩展。我在MainActivity里只做两件事:初始化ViewPager2和TabLayout,管理Fragment容器的生命周期。具体的业务代码全部下沉到各个Fragment里。
以“消息”Fragment为例,它的布局结构是:一个RecyclerView占满全屏,顶部隐藏一个搜索栏(可以暂不实现)。加载数据时,先创建List<Message>,然后设置RecyclerView的LayoutManager和Adapter。
RecyclerView的LayoutManager用LinearLayoutManager,方向为VERTICAL。不要用GridLayoutManager,消息列表是典型的信息流列表,不是九宫格。
列表分割线用DividerItemDecoration实现:
DividerItemDecoration divider = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL); divider.setDrawable(ContextCompat.getDrawable(this, R.drawable.divider_bg)); recyclerView.addItemDecoration(divider);divider_bg是一个高度为0.5dp的灰色Drawable。注意系统自带的DividerItemDecoration在Android 9以下需要自己设置颜色,否则分割线可能是默认黑色。
其他三个Fragment可以先不塞列表,放一个居中的TextView显示对应页面的名称。这样主界面的整体结构已经完整了,后面你有空再一个一个补充真实内容。有人可能会觉得这样太简陋,但我的观点是:骨架先立起来,皮肉慢慢长。如果你一上来就想着把每个页面都做成完整的样子,很容易陷入细节,反而把主界面的框架结构抛在脑后。
3.6 界面细节增强:状态栏与沉浸式效果
仿QQ的登录界面,状态栏是浅色背景配深色图标的,主界面则是白色状态栏。这个细节看起来很“小”,但直接影响观感。
实现方式如下:
// 登录页 Window window = getWindow(); window.setStatusBarColor(Color.parseColor("#FFFFFF")); window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);第一行把状态栏背景设置为白色,第二行把状态栏图标设置为深色。如果不设置LIGHT_STATUS_BAR,状态栏会保持白色背景但图标也是白色,直接看不见。
如果追求“沉浸式”,可以配合WindowInsetsController把状态栏做成透明,让登录页的Logo区域直接延伸到状态栏后面。但沉浸式会带来一个布局适配问题:输入框的paddingTop需要额外加上状态栏高度,否则输入框会被状态栏遮住。这部分可以通过ViewCompat.setOnApplyWindowInsetsListener动态获取状态栏高度,再设置给根布局。我建议初学者先只做颜色适配,沉浸式留到后面熟练了再搞。
4. 常见问题与排查技巧实录
4.1 输入框无法弹出软键盘的问题
这个问题的表现是:点击EditText,光标出现了,但软键盘死活不弹。根源大多是AndroidManifest.xml里MainActivity设置了android:windowSoftInputMode="stateHidden",而且没有adjustPan或者adjustResize配合。
正确做法是在登录页的activity标签里这样配置:
<activity android:name=".activity.LoginActivity" android:windowSoftInputMode="adjustResize|stateHidden" />adjustResize的作用是当软键盘弹出时,自动调整布局尺寸,让输入框不被遮挡。stateHidden则是页面初次展示时隐藏键盘。两者并不冲突。
如果你发现键盘弹出来之后,登录按钮被键盘遮住了,那说明adjustResize没有生效。检查一下根布局是不是ScrollView,如果是,键盘弹出时布局尺寸调整可能没法正确传递。更好的方案是根布局用LinearLayout或ConstraintLayout,配合adjustResize就能自动压缩高度。
4.2 ViewPager2与TabLayout关联后白屏
TabLayoutMediator关联之后,页面完全不显示,或者只显示第一个页面。大部分时候问题出在FragmentStateAdapter的构造方法上。ViewPager2的设计要求FragmentStateAdapter在构造时传入FragmentActivity或Fragment,如果你错误地传入了一个普通的Context,ViewPager2无法创建对应的Fragment容器,自然就白屏了。
另一个常见原因是Fragment的空构造方法缺失。如果你在Fragment里写了带参数的构造方法,一定要补一个无参构造,否则系统重建Fragment时会直接抛异常。这种崩溃通常只在屏幕旋转或进程被杀后恢复时出现,平时正常启动反而看不出问题,排查起来特别隐蔽。
4.3 RecyclerView数据刷新后不显示
列表页的RecyclerView加载完数据还是不显示,先检查两个地方。第一,Adapter的getItemCount()是否正确返回了数据长度;第二,LayoutManager有没有设置。这两个是基础,但不是唯一原因。
还有一个容易踩的坑:你在Fragment里创建的RecyclerView,在onCreateView里设置完Adapter后,又在onViewCreated里重新setAdapter覆盖了。这种情况在代码复制粘贴时特别容易出现。建议把RecyclerView的初始化统一放到onViewCreated里,保证只执行一次。
如果数据更新了但界面没变化,notifyDataSetChanged()仍然无效,那就要怀疑是否在子线程更新了数据。RecyclerView必须在主线程更新,子线程操作会直接抛CalledFromWrongThreadException,但有时候异常被吞掉了,表现为界面不刷新。这种情况可以在Adapter里加一个assert(Looper.myLooper() == Looper.getMainLooper())来提前发现。
4.4 图片加载崩溃与OOM问题
仿QQ项目里,如果你用头像资源,直接加载大图很容易触发OutOfMemoryError。在模拟器上可能不明显,在真机上特别是低端机型上,一个几百KB的图片解码成Bitmap后,内存占用可能是原始大小的好几倍。
如果你不希望引入图片加载库,最直接的方案是用BitmapFactory.Options做降采样。设置inSampleSize=2,图片宽高会缩小到原来的二分之一,内存占用直接降到四分之一。具体写法是先用inJustDecodeBounds=true读取图片尺寸,根据目标宽高计算inSampleSize,再重新解码。
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), resId, options); int targetWidth = 100; // 目标宽度 int sampleSize = 1; while (options.outWidth / (sampleSize * 2) >= targetWidth) { sampleSize *= 2; } options.inJustDecodeBounds = false; options.inSampleSize = sampleSize; Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);但说句实在话,如果项目里要加载的图片超过三张,直接用Glide是更实际的选择。Glide不仅帮你处理了内存缓存和磁盘缓存,还解决了生命周期绑定问题,页面销毁后会自动取消加载。为了练手写一遍降采样是好事,但不要为了“不用第三方库”而不用第三方库,工具是拿来用的。
4.5 默认返回键行为不符合预期
登录成功跳转到MainActivity后,如果在主界面按返回键,一般期望是退出应用,而不是返回到登录页。前面我提过跳转时调用finish(),但有些场景下你可能会忘记在LoginActivity里调用。
还有一种常见情况:从主界面退出后再点应用图标,期望直接进入登录页而不是回到之前的登录状态。如果你没有做登录状态的持久化处理,应用冷启动后默认就是登录页,反而没问题。真正麻烦的是应用进程没有被杀死时,按Home键退到桌面,再点图标回来,会直接恢复之前的主界面状态。这是Android的默认行为,不算bug,但如果你的模拟项目里登录状态是假的(没做持久化),这种恢复会显得不真实。解决方案是判断SharedPreferences里是否有登录态,有就跳到主界面,没有就留在登录页。
5. 从实战中沉淀的经验与改造方向
我实际做完这个仿QQ项目后,最大的感受是:很多问题不是看教程能看出来的,非得自己亲手敲一遍才能体会到。比如TabLayoutMediator的attach()那行代码,我第一遍写的时候漏掉了,页面怎么都不出效果,查了两小时才找到原因。这种记忆比任何教程都牢固。
如果你学有余力,建议在基础版之上做三个方向的扩展:
第一个方向是数据层落地。把静态的List<Message>换成Room数据库存储,消息列表的数据从库里读取。这样你顺便把Android的数据库方案也学了。
第二个方向是接入真实网络。用一个开源的IM SDK或者自己搭一个简单的WebSocket服务,把登录和消息收发改成真实网络请求。这一步会极大扩展项目的实用性,也能让你了解UI和网络是怎么配合的。
第三个方向是迁移到Kotlin和Compose。当你把传统View体系的这个项目摸透之后,用Jetpack Compose重写一遍登录页和主界面。这种“对比式学习”能让你同时理解新旧两套UI体系的差异,面试时被问到相关问题也能答得很从容。
我在实际操练中还有一个体会:这个项目完成后,把它作为面试作品放在简历上,效果比写一堆“精通XX框架”要有说服力得多。面试官问“你做过什么项目”,你能把登录页面的状态管理、主界面的Fragment组织、自动登录的持久化方案讲清楚,就已经赢了大部分只会背概念的人。
最后再分享一个小技巧:做完一个版本之后,把代码用Git提交一个tag,比如v1.0-basic。后面你每做一次大重构,都新建一个分支。这样你随时可以回看自己从简陋到完善的全过程,不仅对技术成长有帮助,还能在复盘时看到自己思维的演变轨迹。