最近在整理贴吧 Android 客户端个人中心的主态与客态页面。所谓主态,是用户查看自己的个人中心;客态,则是用户进入其他人的主页。
两个页面在视觉结构上很相似:头像、昵称、等级、个人信息和发布内容都可能存在。但一旦进入交互细节,它们又明显不是同一个页面:
- 查看自己时,用户关心的是编辑资料、管理内容和进入自己的功能入口。
- 查看别人时,用户关心的是关注、查看内容,以及双方关系带来的操作变化。
- 在未登录、内容受限或关系状态变化时,页面还会出现更多边界情况。
如果只从布局出发,这个需求很容易被理解成“复用一份个人主页 UI,再加几个判断”。但真正困难的是如何描述这个页面所处的状态,并让数据请求、控件显示和用户操作始终保持一致。
主态和客态不是两个静态页面
最直观的方案,是把自己的主页和他人的主页分别实现:
MyProfileActivityOtherProfileActivity
这样在需求最初看起来比较清楚:自己的页面写自己的功能,别人的页面写关注等功能。但随着业务继续增加,共享部分会越来越多:
- 头像、昵称、等级等资料头部。
- 主题帖、回复或其他内容列表。
- 下拉刷新和分页加载。
- 点击内容进入详情。
- 异常状态和空状态。
两份页面分别维护,不仅容易重复,也可能造成同一份资料在两处显示不一致。
另一种极端,是把所有行为塞进一个页面,然后在每个地方判断当前是不是自己:
if (isSelf) { mEditButton.setVisibility(View.VISIBLE); mFollowButton.setVisibility(View.GONE);} else { mEditButton.setVisibility(View.GONE); mFollowButton.setVisibility(View.VISIBLE);}
这个写法在两个按钮时没有问题。一旦差异扩散到菜单、列表操作、空状态、数据接口、埋点和登录处理,isSelf 会散落在页面各处,后续每一次改动都需要小心检查所有分支。
所以,这类页面首先需要解决的并不是“拆成几个文件”,而是页面状态应该如何建模。
先确定访问模式,再渲染页面能力
个人中心最基础的差异来源,是“当前用户正在看谁”。可以先把访问模式明确下来:
public enum ProfileMode { SELF, VISITOR}
打开页面时,根据登录用户 id 与目标用户 id 判断模式:
private ProfileMode resolveProfileMode(String loginUserId, String profileUserId) { if (!TextUtils.isEmpty(loginUserId) && TextUtils.equals(loginUserId, profileUserId)) { return ProfileMode.SELF; } return ProfileMode.VISITOR;}
这里将未登录用户访问某个主页也视为访客模式。未登录会进一步影响具体操作,例如点击关注时需要先引导登录,但它不改变“正在查看别人的资料”这个页面事实。
确定 ProfileMode 后,页面头部可以集中处理主客态的基础差异:
private void renderActions(ProfileMode mode) { boolean isSelf = mode == ProfileMode.SELF; mEditProfileButton.setVisibility(isSelf ? View.VISIBLE : View.GONE); mFollowButton.setVisibility(isSelf ? View.GONE : View.VISIBLE); mMessageButton.setVisibility(isSelf ? View.GONE : View.VISIBLE); mManageEntry.setVisibility(isSelf ? View.VISIBLE : View.GONE);}
这比在每一次按钮点击、请求回调或者列表渲染里临时判断要稳定一些。页面先拥有明确模式,再由模式决定可见能力。
访问模式之外,还有关系状态
客态页面也不是一种固定样子。查看其他用户时,关注关系可能处于不同状态:
public enum FollowState { NOT_FOLLOWED, FOLLOWED, MUTUAL, LOADING}
这些状态会直接影响按钮文案和可执行操作:
| 状态 | 按钮展示 | 用户操作 |
|---|---|---|
| 未关注 | 关注 | 发起关注 |
| 已关注 | 已关注 | 取消关注或展示菜单 |
| 互相关注 | 互相关注 | 维持或取消关系 |
| 请求中 | 操作中 | 暂时禁止重复点击 |
因此,完整的客态展示不能只依赖 ProfileMode.VISITOR,还需要结合资料接口返回的关系状态:
private void renderFollowState(FollowState state) { switch (state) { case NOT_FOLLOWED: mFollowButton.setText("关注"); mFollowButton.setEnabled(true); break; case FOLLOWED: mFollowButton.setText("已关注"); mFollowButton.setEnabled(true); break; case MUTUAL: mFollowButton.setText("互相关注"); mFollowButton.setEnabled(true); break; case LOADING: mFollowButton.setText("处理中"); mFollowButton.setEnabled(false); break; }}
这件事看起来只是一个按钮,但它体现了一类很常见的页面问题:身份决定能力范围,业务状态决定能力当前如何表现。
共享资料展示,分离行为区域
主态和客态页面虽然操作不同,但资料展示往往高度一致。比较合理的页面结构是:
Profile Page -> Header:头像、昵称、等级、签名等公共资料 -> Action Area:编辑资料 / 关注 / 私信等差异行为 -> Tab Area:主题、回复等内容维度 -> Content List:分页内容列表
公共资料区域可以只绑定统一的数据模型:
public class ProfileInfo { public String userId; public String userName; public String avatarUrl; public String signature; public int level; public int postCount; public FollowState followState;}
页面获取到资料后,先渲染共享信息,再根据访问模式和关系状态渲染操作区域:
private void renderProfile(ProfileInfo profile) { mUserNameView.setText(profile.userName); mSignatureView.setText(profile.signature); mLevelView.setText(String.valueOf(profile.level)); loadAvatar(profile.avatarUrl); renderActions(mProfileMode); if (mProfileMode == ProfileMode.VISITOR) { renderFollowState(profile.followState); }}
这样做的好处是,共享信息的展示规则只有一份;主客态的差异被限制在明确的行为区域中,不会轻易蔓延到整张页面。
内容列表复用,但操作权限要独立处理
个人中心里的内容列表,同样会面临“内容相似、行为不同”的问题。
例如主题帖列表本身可能都展示标题、摘要、时间和互动数据,但在主态下,自己的内容可能允许删除或管理;在客态下,只允许进入详情或执行举报等操作。
这里不需要为主客态各维护一套列表适配器。可以尽量复用列表展示,再将对单条内容可执行的能力作为配置传入:
public class ProfileListPermission { public boolean canDelete; public boolean canManage; public boolean canReport;}
根据访问模式生成权限:
private ProfileListPermission buildListPermission(ProfileMode mode) { ProfileListPermission permission = new ProfileListPermission(); if (mode == ProfileMode.SELF) { permission.canDelete = true; permission.canManage = true; permission.canReport = false; } else { permission.canDelete = false; permission.canManage = false; permission.canReport = true; } return permission;}
列表负责按照权限显示菜单和分发点击事件,页面负责真正执行删除或举报操作。这样列表视图仍然能够共享,而权限边界也比到处判断 isSelf 更清晰。
关注操作需要处理即时反馈与失败回滚
在客态个人中心里,关注按钮是典型的即时交互。用户点击后,希望立刻看到结果;但服务端请求也可能失败。
一种偏保守的方式,是先展示加载状态,请求成功后再更新:
private void followUser() { renderFollowState(FollowState.LOADING); mProfileRepository.follow(mProfileUserId, new Callback() { @Override public void onSuccess() { renderFollowState(FollowState.FOLLOWED); } @Override public void onFailure() { renderFollowState(FollowState.NOT_FOLLOWED); showToast("关注失败,请重试"); } });}
如果产品希望交互更加快速,也可以先乐观更新按钮状态,请求失败后再回滚。不管采用哪一种方式,重要的是关注结果不能只更新当前按钮:
- 如果页面显示粉丝或关注数量,需要同步变化。
- 如果从个人中心返回列表,列表中的关注状态也应保持一致。
- 如果用户重复点击,需要避免同时发出多次互相冲突的请求。
这也是复杂业务页面比普通静态页面困难的地方:一个看似局部的动作,可能会影响多个信息展示位置。
刷新与页面回退也会改变状态
主态页面进入资料编辑页以后,用户修改了头像或签名,再回到个人中心,头部数据需要刷新。
客态页面则可能从关注列表、帖子详情等其他入口返回,这期间关系状态或内容数据也可能已经变化。
页面不应简单假设首次加载得到的数据一直有效。可以针对明确发生变化的场景设置刷新标记:
private boolean mProfileChanged;@Overrideprotected void onResume() { super.onResume(); if (mProfileChanged) { loadProfile(); mProfileChanged = false; }}
同时,也需要避免每一次返回页面都无条件刷新全部接口,否则会增加请求量,也可能造成明显的页面闪动。对于个人资料、关系列表和帖子列表,哪些变化需要立即更新,哪些可以下次下拉刷新再同步,需要根据用户感知程度做判断。
空状态也属于主客态差异
内容为空时,主态和客态的文案与操作入口通常也不应该完全相同:
- 自己还没有发布过内容,可以提示用户去发表第一条内容。
- 别人没有可查看内容,则只需要表达当前没有展示内容。
- 未登录状态下访问某些入口,可能还需要提供登录引导。
如果只关注“有数据时如何显示”,就很容易遗漏这些状态下的体验差异。
private void renderEmptyState(ProfileMode mode) { if (mode == ProfileMode.SELF) { mEmptyText.setText("还没有发布内容,去说点什么吧"); mCreateButton.setVisibility(View.VISIBLE); } else { mEmptyText.setText("暂时没有可查看的内容"); mCreateButton.setVisibility(View.GONE); }}
从工程上看,空状态不是一个额外的占位页面,而是业务状态模型中的组成部分。
这类业务需要先整理状态边界
个人中心主客态的迭代,表面上是页面、按钮和接口的不断调整,真正需要处理的是页面状态的边界:
- 当前查看的是自己还是别人。
- 用户是否已经登录。
- 双方处于什么关系状态。
- 当前内容是否允许某项操作。
- 操作完成后,哪些区域必须同步更新。
- 页面离开再回来时,哪些数据可能已经失效。
如果这些问题没有提前整理清楚,页面就容易变成许多互相影响的条件分支。相反,如果先定义访问模式、关系状态与操作权限,视图本身反而会简单很多。
小结
个人中心的主态与客态,看起来是一个常见的业务页面需求,却很适合作为理解客户端状态建模的例子。
复杂页面的复用并不是把所有内容放进同一个 Activity 或 Fragment 就结束了。真正有效的复用,是把共享展示、身份差异、关系变化和操作权限分别放到清晰的位置,让用户每一次操作之后看到的页面始终可信。