
很多 Android 实战项目做到第五个模块的时候“我”的页面往往会被当成一个收尾的简单活儿。课程列表、播放器、搜索页都比它看起来有技术含量所以轮到这个模块时大家容易觉得只是摆个头像、放几个菜单按钮。可真的动手之后才发现这一页是整个应用里信息入口最杂的地方用户登录状态、头像上传、菜单跳转、退出清理每一个点都和前面四期模块强相关。我最近在跟进的这个基于 Eclipse 的在线教育项目第五期正好就是“我”的模块。借着这次整理的机会我把这一页从数据字段到页面跳转重新拆了一遍把登录缓存、头像选择、未登录拦截这些容易返工的部分一起讲清楚。如果你也在维护老工程或者正在做一个底部导航类的普通内容型 App这篇东西应该能帮你少踩几个坑。1. 先把模块定位说透“我”的页面不是静态页而是用户信息的收口1.1 页面分成两层信息头与功能菜单职责完全不同“我”的模块在大多数 App 里承担两类任务第一类是把当前登录用户的基本信息展示出来第二类是提供一个和账户、偏好相关的功能入口集合。这两类内容在视觉上往往是叠在一起的但分析需求时最好分开看否则后面很容易把布局和事件混成一锅粥。以我们这个在线教育项目为例“我”的页面从上到下大致是这样顶部信息头用户头像、昵称、一句个人签名、可能还有学习天数或积分。中间功能菜单我的课程、我的收藏、意见反馈、设置、检查更新、退出登录。顶部信息头的数据依赖很强它需要用户登录之后从服务端拿到的资料属于“有状态”的部分。中间功能菜单则相对独立大多数入口只是跳转到一个新页面少数需要判断是否登录比如“我的课程”必须登录才能看。所以实现的时候我会把这两个区域分开处理信息头用组合布局拼出来菜单区域单独做事件绑定。这样改动菜单项不会影响头像区反之亦然。1.2 先从数据字段反推页面而不是先画布局很多新手写“我”的页面上手就拖 ImageView 和 TextView等拖完了才发现缺字段。更合理的顺序是先定义用户实体类把这一页需要的数据字段定死再去写布局。在我们项目里用户实体类大致是下面这些字段字段类型说明页面上的位置userIdString用户唯一标识很少直接展示但跳转时需要nicknameString昵称信息头中间位置avatarUrlString头像地址或本地文件路径信息头顶部圆形头像signatureString个人签名昵称下方一行小字tokenString登录凭证不展示但接口请求要用mobileString手机号一般在设置页或账号安全页展示genderint性别可能影响展示或推荐逻辑这些字段不是全部都要显示在“我”的主页上但用户实体类必须留好因为从这一页跳出去的二级页面比如我的课程、我的收藏都会共用同一个用户对象。1.3 和前面四期模块的耦合点在哪里这个模块虽然排在第五期但它和前面几期的关系非常紧密。登录功能一般在第一期或第二期就已经完成“我”的页面必须读取登录后保存的用户数据。首页和课程列表可能用到同一个用户 token而退出登录时也要考虑要不要清掉别的模块的缓存数据。在这个项目里前面几期已经有一个全局的 LoginHelper 类里面保存了登录状态和用户基本信息的缓存。所以“我”的模块在设计时就不需要重复造一套用户状态管理而是直接复用这套缓存。这个决定很重要因为如果你在“我”模块里又单独维护一套用户数据很容易出现登录之后首页已经读到新数据而“我”页还是旧数据的情况。2. Eclipse 老工程里的布局拼装不引第三方库也能把“我”页做干净2.1 顶部信息头用 RelativeLayout 叠加背景结构最省事那个年代的 Eclipse 工程里第三方依赖管理没有 Android Studio 方便为了做一个渐变背景和圆形头像去引一个大库并不划算。所以信息头我直接用了 RelativeLayout背景用一个 shape drawable 代替渐变头像用系统自带的 ImageView 加圆形裁剪代码少兼容性也容易控制。给一个当时比较典型的布局片段RelativeLayout android:idid/headerLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:backgrounddrawable/bg_header_gradient android:paddingTop24dp android:paddingBottom20dp ImageView android:idid/ivAvatar android:layout_width64dp android:layout_height64dp android:layout_alignParentToptrue android:layout_centerHorizontaltrue android:srcdrawable/default_avatar android:contentDescription头像 / TextView android:idid/tvNickname android:layout_widthwrap_content android:layout_heightwrap_content android:layout_belowid/ivAvatar android:layout_centerHorizontaltrue android:text未登录 android:textSize18sp android:textColor#FFFFFF / TextView android:idid/tvSignature android:layout_widthwrap_content android:layout_heightwrap_content android:layout_belowid/tvNickname android:layout_centerHorizontaltrue android:text这个人很懒什么都没有写 android:textSize13sp android:textColor#CCFFFFFF / /RelativeLayout因为整个模块都是基于 Eclipse 老工程当时的 res 目录里没有太多自动生成的资源所以背景我都放在 drawable 文件夹下shape 文件可以直接复用。头像默认图也提前放在 drawable 里避免用户没登录时显示一个空白图片。2.2 功能菜单用 LinearLayout 而不是再套一层 ListView很多教程会把菜单区域做成 ListView配一个 ArrayAdapter看起来比较“正规”。但这个在线教育项目里的菜单项只有六项左右数据固定没有动态增删需求再套一个 ListView 反而增加了很多无关代码。我更推荐直接用一个垂直的 LinearLayout每个菜单项用一个小布局文件 include 进来或者直接在 XML 里写三个 TextView 和三条分割线。这样做的最大好处是点击事件可以直接绑定不需要处理 ListView 的 Item 点击和 ViewHolder 复用也更容易看清每个菜单项的归属。菜单项之间的分割线并不用专门切图直接用 shape drawable 可以做出一条浅灰色分割线View android:layout_widthmatch_parent android:layout_height0.5dp android:background#E5E5E5 /当然如果项目里的菜单项已经变成了服务端动态下发需要用 ListView 或 RecyclerView 来做那就是另一种情况。但就当前这种固定菜单的实战项目LinearLayout 足够。2.3 drawable 里的圆角背景和选中态别忽略按下效果移动端页面最怕的是点上去没有任何反馈。看起来是小事但真实体验就是“觉得卡”。Eclipse 工程里做点击反馈最简单的方式不是去引入第三方水波纹库而是定义一个 selector。假如我们给“退出登录”按钮做一个红色渐变底按下变深色可以这样写selector xmlns:androidhttp://schemas.android.com/apk/res/android item android:state_pressedtrue shape solid android:color#CC3B3B / corners android:radius6dp / /shape /item item shape solid android:color#F24646 / corners android:radius6dp / /shape /item /selector这个 selector 文件放在 drawable 下然后给 Button 设置 android:background 即可。以前很多同学直接把背景色写死在布局里按下去没有任何变化我只能说这种细节在功能演示时可能无所谓在实际上手用的时候特别明显。3. 用户登录信息怎么存才能让“我”页每次刷新都不出错3.1 登录成功之后第一步不是跳转而是写入本地缓存“我”的模块要展示的信息来自登录接口的返回值。在 Eclipse 老项目里我通常直接使用 SharedPreferences 来保存用户信息。为什么不存 SQLite因为单个用户的资料复用简单无需复杂查询SharedPreferences 的开销更低代码也更直观。这里把登录成功后的写入动作拆成一个小方法放在工具类里统一调用。代码非常简单关键是字段名要和读取端保持一致public static void saveUserInfo(Context context, User user) { SharedPreferences sp context.getSharedPreferences(user_info, Context.MODE_PRIVATE); SharedPreferences.Editor editor sp.edit(); editor.putString(userId, user.getUserId()); editor.putString(nickname, user.getNickname()); editor.putString(avatar, user.getAvatarUrl()); editor.putString(signature, user.getSignature()); editor.putString(token, user.getToken()); editor.apply(); }我习惯用 apply() 而不是 commit()因为 apply 会异步写入磁盘不阻塞主线程在登录跳转这种场景下体验更顺。Eclipse 期的 Android 2.x/3.x 时代 commit 更常见但现在回看还是该早一点切 apply。3.2 “我”页的 onCreate 里恢复数据注意给足默认值用户从登录页跳回主界面之后“我”的模块会执行 onCreate。此时应当直接从 SharedPreferences 读取数据并刷新 UI。读取时最容易犯的错误是只取一个字段然后直接 setText一旦这个字段不存在页面就会显示 null。正确做法是每个字段都给默认值String nickname sp.getString(nickname, 未登录); String signature sp.getString(signature, 这个人很懒什么都没有写); String avatar sp.getString(avatar, );昵称给“未登录”签名给默认文案头像为空时就用 drawable 里的默认头像。这样即使本地缓存被清理页面也不会出现丑陋的“null”。另外要注意如果头像字段保存的是本地文件路径读取后需要做一次文件是否存在判断否则文件被清理后 ImageView 仍然会跑去加载一个不存在的文件。3.3 退出登录不止跳转页面缓存和 Activity 栈都要清退出登录是“我”模块里很容易做漏的功能。很多同学写退出时只是弹一个确认框然后跳到登录页这样会留下很多隐患。至少在退出登录时应该做三件事第一清掉 SharedPreferences 里保存的用户信息和 token。不清掉的话用户下次打开 App 还会处于“已登录”的假状态。第二把第三方页面、WebView 本地 Cookie 中与登录相关的内容清理掉。第三把用户之前缓存的头像文件路径失效。在 Eclipse 老工程里退出后我一般会跳到一个纯粹的登录页并把之前所有 Activity 关闭掉。有一种简单写法是给登录页的 Intent 加 FLAG_ACTIVITY_CLEAR_TASK 和 FLAG_ACTIVITY_NEW_TASK这样可以让返回键不会把你带回到用户已退出的主界面。4. 菜单交互、头像选择和未登录拦截实战细节全在这里4.1 用一个 OnClickListener 区分所有菜单项不要写一堆匿名内部类“我”的模块有六个左右的菜单项如果每个都写一个 setOnClickListener代码会显得很冗余。我通常让整个页面 Activity 实现 View.OnClickListener然后在 findViewById 之后把菜单项都设进去findViewById(R.id.llMyCourse).setOnClickListener(this); findViewById(R.id.llMyCollection).setOnClickListener(this); findViewById(R.id.llFeedback).setOnClickListener(this); findViewById(R.id.llSettings).setOnClickListener(this); findViewById(R.id.llCheckUpdate).setOnClickListener(this); findViewById(R.id.llLogout).setOnClickListener(this);然后在 onClick 方法里按 view.getId() 分发Override public void onClick(View v) { switch (v.getId()) { case R.id.llMyCourse: if (checkLogin()) { startActivity(new Intent(this, MyCourseActivity.class)); } break; case R.id.llSettings: startActivity(new Intent(this, SettingsActivity.class)); break; case R.id.llLogout: showLogoutDialog(); break; default: break; } }这种写法的好处是后面再加菜单项只需要新增一个 case 分支不需要再去翻一大堆匿名类。缺点是如果菜单项特别多switch 结构会显得很长但六项以内完全没问题。4.2 头像从相册和相机选择的兼容写法头像点击是“我”页里最折磨人的功能之一。因为涉及相册调起、拍照、裁剪、权限申请还有 Android 7.0 之后的 FileProvider 适配。如果是在 Eclipse 项目里做我们需要处理两种选择方式一种是从相册选图一种是调系统相机拍照。老一点的教程会让你直接用 Uri.fromFile(file)但如果 App 在 Android 7.0 及以上运行这个写法一定会出 FileUriExposedException 崩溃。所以虽然工程很老适配还是要补上。简单做法是第一步在 AndroidManifest.xml 里注册 FileProvider 对应的 provider。第二步拍照时把文件路径通过 FileProvider.getUriForFile 转成 content:// URI。第三步用 Intent 调起系统相机或者用 Intent.ACTION_GET_CONTENT 打开相册。这里不能把代码细节全部堆进来因为不同工程包名和配置不一样但套路是一致的。在 Eclipse 里做的时候尤其要注意 provider 的 authorities 要和包名保持一致。如果不处理这个只能在低版本模拟器上勉强跑通真机上大概率闪退。4.3 未登录状态下点击菜单是返回提示还是直接跳登录页很多“我”页在未登录时点击“我的课程”会直接跳到登录页。看似简单但这里有一个体验细节未登录的用户可能在首页也有入口他不一定是从“我”页跳过来的。所以在拦截逻辑里不要写“你已经退出登录”这种硬话直接跳登录页更自然。更稳妥的写法是使用 startActivityForResultprivate boolean checkLogin() { if (isLogin()) { return true; } Intent intent new Intent(this, LoginActivity.class); startActivityForResult(intent, REQUEST_CODE_LOGIN); return false; }登录页返回之后在 onActivityResult 里判断登录结果如果登录成功再自动跳转到目标菜单页。这一步提醒很有价值因为很多同学只是跳到登录页用户登录回来发现自己被丢在一个空白页只能再点一次菜单体验非常割裂。在 Eclipse 项目里我们常用 setResult 和 onActivityResult 这套老机制虽然代码比现在的 Activity Result API 啰嗦但逻辑完全走得通。5. Eclipse 构建过程中最容易翻车的资源问题以及我的排查顺序5.1 R 类突然消失第一反应不应该是 clean 项目Eclipse 开发 Android 的都知道工程里 R.java 是一个自动生成的类。如果 XML 布局里有一个标签写错或者资源文件放了不合法的命名R 类就会直接消失。很多人的第一反应是右键 clean project但很多时候 clean 并不能解决真正的问题反而浪费时间。我的排查顺序是先看 Problems 窗口里有没有 XML 解析错误再看 res 目录下是不是有文件名以大写字母开头或包含减号最后再看 drawable 里的资源是不是引用了不存在的图片。如果这几个都没问题再试 clean。R 类消失的时候代码里所有 R.id 和 R.layout 引用都会飘红但你不可能逐个去改因为根因都在资源文件层面。5.2 资源命名规范小写字母和数字组合最安全Eclipse 时代资源命名比 Android Studio 时代严格得多而且 R 类生成失败最容易由不规范的命名触发。我印象最深的是一次把图片命名为 avatar_bg_v2.0.png中间带了个点号Resource 编译直接挂掉。正确的命名规范是全部使用小写英文字母、数字和下划线并且不能以数字开头。比如 header_bg.png、icon_logout.png、bg_menu_selector.xml 都是安全命名。反例就是 AAAAA.jpg 或者 menu-item.xml这类文件名在 Linux 环境下没问题但 Android 构建工具不接受。等到处理 “我”的模块时建议把所有新加的资源都先按这个规则命名再复制进 res 目录。5.3 老 Eclipse 工程和较新 SDK 混用时的主题与 ActionBar 坑很多老工程至今还用的是 ActionBar 方案而不是 Material 风格的主题。在模拟器上跑“我”页时不同 API level 的默认主题差异会导致标题栏颜色不一致甚至布局上下留白。我在这类项目里的习惯是不在“我”页依赖系统 ActionBar而是自己做一条简单的标题栏布局。这样可以在 res 里定义一个公共 title layout让这个在线教育项目的所有二级页面都统一使用。标题栏左边放返回按钮中间显示标题右侧偶尔放操作按钮。这样做的好处是页面风格统一不受系统主题升级影响维护成本也很低。如果项目想要更现代的沉浸式效果可以再配合 statusBar 颜色设置但考虑到这是 Eclipse 老工程不建议引入太新的设计依赖稳定优先。最后分享一个这次做“我”模块的真实心得页面本身不难难的是“我”这个字背后牵扯的用户状态管理。如果你做“我”页时发现代码越写越多先停下来看看是不是数据存储没有统一是不是每个菜单都单独写了一遍跳转逻辑。把这些公共问题解决掉这一页其实很快就能完成。后面如果要扩展学习积分、会员入口、消息通知这些东西你会发现提前把菜单分发机制和用户缓存抽好能省掉大量重复工作。