Skip to content
Go back

如何组织贴吧个人中心的主态与客态页面

最近在整理贴吧 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);    }}

从工程上看,空状态不是一个额外的占位页面,而是业务状态模型中的组成部分。

这类业务需要先整理状态边界

个人中心主客态的迭代,表面上是页面、按钮和接口的不断调整,真正需要处理的是页面状态的边界:

如果这些问题没有提前整理清楚,页面就容易变成许多互相影响的条件分支。相反,如果先定义访问模式、关系状态与操作权限,视图本身反而会简单很多。

小结

个人中心的主态与客态,看起来是一个常见的业务页面需求,却很适合作为理解客户端状态建模的例子。

复杂页面的复用并不是把所有内容放进同一个 ActivityFragment 就结束了。真正有效的复用,是把共享展示、身份差异、关系变化和操作权限分别放到清晰的位置,让用户每一次操作之后看到的页面始终可信。


Share this post on: