diycode 开发日志 (五) 使用抽象类节省 1000 行代码

Android · sloop · 于 发布 · 最后由 sloop回复 · 587 次阅读
1644

diycode v0.1.0 发布了,你可以到这里 查看更新查看源码

这是发布的第 5 个版本,修正了之前版本存在的问题,并添加了一点新功能,可以看到这次版本号从 v0.0.6 直接跳到了 v0.1.0,是一次比较大的变动,但是当你打开 app 会发现界面内容变化并不大。

这是因为在新版本中主要是优化了内部代码逻辑,虽然 UI 变化不大,甚至新添加了一个页面,和一些新功能,代码总量却减少了几百行,相关的几个页面代码量更是减少了近 1000 行,正是因为如此,我才特地水一篇文章,来讲讲我的优化思路。

其实优化思路很简单,就是把 复制粘贴(Ctrl+C/V) 进行抽象处理。

在 diycode 里面又很多页面其实是很相似的,但又存在区别。例如首页的三个页面 topic、news、sites、通知、用户话题、用户收藏等。

S70410-01500766-2

虽然看起来有点不太一样,但它们都是 RecyclerView,还是有很多相似内容的。

它们有很多共同的特点:

  1. 主体都是 RecyclerView。
  2. 都具有下拉刷新。
  3. 都具有分页加载。
  4. 都需要管理状态(正常,正在刷新,正在加载,刷新成功,加载成功,数据获取失败)。

也有很多不同的特点:

  1. 请求的数据类型不同。
  2. 条目内容不同。
  3. 布局不同。

所以我们就要想办法把它们的共性抽取出来一同处理,不同的地方让自己去实现,先看一下完全抽象处理后的一个页面代码。这个是首页的 topic fragment,它具有 下拉刷新、上拉加载、数据缓存、滚动状态保存和恢复(即从哪个位置退出的,下次进入还是这个位置)等功能:

/**
 * 首页 topic 列表
 */
public class TopicListFragment extends SimpleRefreshRecyclerFragment<Topic, GetTopicsListEvent> {

    private boolean isFirstLaunch = true;

    public static TopicListFragment newInstance() {
        Bundle args = new Bundle();
        TopicListFragment fragment = new TopicListFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override public void initData(HeaderFooterAdapter adapter) {
        // 优先从缓存中获取数据,如果是第一次加载则恢复滚动位置,如果没有缓存则从网络加载
        List<Object> topics = mDataCache.getTopicsListObj();
        if (null != topics && topics.size() > 0) {
            Logger.e("topics : " + topics.size());
            pageIndex = mConfig.getTopicListPageIndex();
            adapter.addDatas(topics);
            if (isFirstLaunch) {
                int lastPosition = mConfig.getTopicListLastPosition();
                mRecyclerView.getLayoutManager().scrollToPosition(lastPosition);
                isFirstAddFooter = false;
                isFirstLaunch = false;
            }
        } else {
            loadMore();
        }
    }

    @Override protected void setAdapterRegister(Context context, RecyclerView recyclerView,
                                                HeaderFooterAdapter adapter) {
        adapter.register(Topic.class, new TopicProvider(getContext()));
    }

    @NonNull @Override protected String request(int offset, int limit) {
        return mDiycode.getTopicsList(null, null, offset, limit);
    }

    @Override protected void onRefresh(GetTopicsListEvent event, HeaderFooterAdapter adapter) {
        super.onRefresh(event, adapter);
        mDataCache.saveTopicsListObj(adapter.getDatas());
    }

    @Override protected void onLoadMore(GetTopicsListEvent event, HeaderFooterAdapter adapter) {
        super.onLoadMore(event, adapter);
        mDataCache.saveTopicsListObj(adapter.getDatas());
    }

    @Override public void onDestroyView() {
        super.onDestroyView();
        // 存储 PageIndex
        mConfig.saveTopicListPageIndex(pageIndex);
        // 存储 RecyclerView 滚动位置
        View view = mRecyclerView.getLayoutManager().getChildAt(0);
        int lastPosition = mRecyclerView.getLayoutManager().getPosition(view);
        mConfig.saveTopicListState(lastPosition, 0);
    }
}

这个是首页 topic 列表的页面,经过抽象处理后,到具体实现部分就只剩下不到一百行的代码,而且哪个方法做什么都很清楚,不会混乱。

虽然最终实现看起来比较简单,但是想要写到这么简单却不容易,在这个项目中,它是基于我自己实现的 MultiTypeAdapter 和抽象的 Fragment 才能实现的。

先说 MultiTypeAdapter,在上面代码中可以看到一个名为 HeaderFooterAdapter 的东西,它本质上是一个 MultiTypeAdapter,为了方便的给 RecyclerView 添加头部和尾部信息菜实现出来的一个类。关于它的代码你可以到 diycode/recyclerview 查看,使用它可以保证 RecyclerView 中的内容不受类型限制。

再说抽象 Fragment,上面的 Fragment 是继承自 SimpleRefreshRecyclerFragment 的,实际上关于 Fragment 我进行了多层抽象,为了易用性和扩展性,其继承结构如下。

Fragment(系统)
  +- BaseFragment
       +- RefreshRecyclerFragment
            +- SimpleRefreshRecyclerFragment
                +- NodeTopicListFragment

先说明一下其中各个 Fragment 的作用:

Fragment 作用
BaseFragment 自定义的 Fragment 基类,主要用于 View 布局管理。
RefreshRecyclerFragment 实现了下拉刷新和上拉加载功能的 Fragment,用于管理状态。
SimpleRefreshRecyclerFragment 实现了部分 RefreshRecyclerFragment 的抽象方法。
NodeTopicListFragment 实现该页面需要实现的部分,网络请求,处理条目内容等。

虽然继承了好几层,看起来类很多,但是具体到每一个类的功能都很明确,简单,只完成一种功能(单一职责),每一个类的代码量都不到 300 行。

而对于其中不同的部分,在超(父)类中无法确定或者无法完成的,就用抽象方法强制子类去实现,这样可以确保子类实现的时候不会遗忘掉这些内容。具体代码实现就不贴在这里了,大家感兴趣的话可以直接点击上面表格中的名称来查看源码。

这种多层抽象有很多好处。

一、可复用性强。

不同层级实现了不同功能,按需继承即可,代码可复用性强。

如果只是一个普通的 Fragment ,那么直接继承 BaseFragment 就可以了。
如果是需要下拉刷新和上拉加载的 Fragment,继承 RefreshRecyclerFragment 可以免去状态管理的烦恼,只用处理网络请求和返回数据就行了。
如果这个 Fragment 不需要对数据进行特殊处理,使用线性布局(LinerLayoutManager),那就更方便了,继承 SimpleRefreshRecyclerFragment 会让你爽到爆,只用手写几行代码完成这个页面的功能。

二、防手残。

毕竟我不是电脑,很容易忽略掉一些细节,而抽象类的抽象方法会强制要求子类实现,一般看到方法名称就知道该方法是做什么的了,可以防止忽略掉一些细节导致程序 bug。

三、修 bug 简单。

假如发现一个共有的 bug ,只需要在上层方法中修复一次即可,简单方便。

也许上面的代码你觉得也就那样,那么和上一个版本的代码对比一下就知道了这份代码有多么好了(代码在文章最后)。上一个版本是抽象处理前的,每个页面代码量接近 300 行,类似的页面一共有 7 个。优化代码可以让每个页面节省近 200 行,也就是说,理论上可以节省 7 x 200 = 1400 行代码。

我觉得如果一个东西复制,粘贴了三次以上,就应该考虑一下是否需要将这一部分优化一下,抽取一个公共方法或者抽象类,否则这部分代码会在后期的修改过程中逐渐腐烂掉,很容易从 1 份代码的 copy 逐渐变成 n 份不同的代码,最后会变的自己都很难去修改。

最后附上 v0.0.6 版本的 topic fragment

/**
 * topic 相关的 fragment, 主要用于显示 topic 列表
 */
public class TopicListFragment extends BaseFragment {
    // 底部状态显示
    private static final String FOOTER_LOADING = "loading...";
    private static final String FOOTER_NORMAL = "-- end --";
    private static final String FOOTER_ERROR = "-- 获取失败 --";
    private TextView mFooter;

    // 请求状态 - 下拉刷新 还是 加载更多
    private static final String POST_LOAD_MORE = "load_more";
    private static final String POST_REFRESH = "refresh";
    private ArrayMap<String, String> mPostTypes = new ArrayMap<>();    // 请求类型

    // 当前状态
    private static final int STATE_NORMAL = 0;      // 正常
    private static final int STATE_NO_MORE = 1;     // 正在
    private static final int STATE_LOADING = 2;     // 加载
    private static final int STATE_REFRESH = 3;     // 刷新
    private int mState = STATE_NORMAL;

    // 分页加载
    private int pageIndex = 0;                      // 当面页码
    private int pageCount = 20;                     // 每页个数

    // 数据
    private Diycode mDiycode;                       // 在线(服务器)
    private DataCache mDataCache;                   // 缓存(本地)

    // View
    private TopicAdapter mAdapter;
    private SwipeRefreshLayout mRefreshLayout;
    NestedScrollView mScrollView;
    private LinearLayoutManager mLinearLayoutManager;

    private boolean isFirstLaunch = true;             // 是否是第一次加载

    private Config mConfig;

    public static TopicListFragment newInstance() {
        Bundle args = new Bundle();
        TopicListFragment fragment = new TopicListFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mConfig = Config.getSingleInstance();
        mDiycode = Diycode.getSingleInstance();
        mDataCache = new DataCache(getContext());
        // 预加载数据, 提前将磁盘数据读取到内存,后续读取更快速
        List<Topic> topics = mDataCache.getTopicsList();
        if (topics != null) {
            for (Topic topic : topics) {
                mDataCache.getTopicPreview(topic.getId());
            }
        }
    }

    @Override
    protected int getLayoutId() {
        return R.layout.fragment_recycler_refresh;
    }

    @Override
    protected void initViews(ViewHolder holder, View root) {
        long time = System.currentTimeMillis();
        Logger.e("time = " + time);
        mFooter = holder.get(R.id.footer);
        mScrollView = holder.get(R.id.scroll_view);
        initRefreshLayout(holder);
        initRecyclerView(getContext(), holder);
        initListener(holder);
        initData();
        Logger.e("initViews 耗时 = " + (System.currentTimeMillis() - time) + " ms");
    }

    // 加载数据,默认从缓存加载
    private void initData() {
        mRefreshLayout.setEnabled(true);
        List<Topic> topics = mDataCache.getTopicsList();
        if (null != topics && topics.size() > 0) {
            // 缓存模式,取出上一次的pageIndex
            pageIndex = mConfig.getTopicListPageIndex();
            mAdapter.addDatas(topics);
            mFooter.setText(FOOTER_NORMAL);
            if (isFirstLaunch) {
                final int lastScroll = mConfig.getTopicLastScroll();
                mScrollView.post(new Runnable() {
                    @Override
                    public void run() {
                        mScrollView.scrollTo(0, lastScroll);
                    }
                });
                isFirstLaunch = false;
            }
        } else {
            loadMore();
            mFooter.setText(FOOTER_LOADING);
        }
    }

    private void initRefreshLayout(ViewHolder holder) {
        mRefreshLayout = holder.get(R.id.refresh_layout);
        mRefreshLayout.setProgressViewOffset(false, -20, 80);
        mRefreshLayout.setColorSchemeColors(getResources().getColor(R.color.diy_red));
        mRefreshLayout.setEnabled(false);
    }

    private void initRecyclerView(final Context context, ViewHolder holder) {
        RecyclerView recyclerView = holder.get(R.id.recycler_view);
        mAdapter = new TopicAdapter(context, mDataCache);
        recyclerView.setAdapter(mAdapter);
        mLinearLayoutManager = new LinearLayoutManager(context);
        mLinearLayoutManager.setSmoothScrollbarEnabled(true);
        mLinearLayoutManager.setAutoMeasureEnabled(true);
        recyclerView.setLayoutManager(mLinearLayoutManager);
        recyclerView.setHasFixedSize(true);
        recyclerView.setNestedScrollingEnabled(false);
        Logger.e("初始化View");
    }

    private void initListener(ViewHolder holder) {
        // 监听 RefreshLayout 下拉刷新
        mRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                refresh();
            }
        });
        // 监听 scrollView 加载更多
        NestedScrollView scrollView = holder.get(R.id.scroll_view);
        scrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
            @Override
            public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                View childView = v.getChildAt(0);
                if (scrollY == (childView.getHeight() - v.getHeight()) && mState == STATE_NORMAL) { //滑动到底部 && 正常模式
                    loadMore();
                }
            }
        });
    }

    // 刷新
    private void refresh() {
        pageIndex = 0;
        String uuid = mDiycode.getTopicsList(null, null, pageIndex * pageCount, pageCount);
        mPostTypes.put(uuid, POST_REFRESH);
        pageIndex++;
        mState = STATE_REFRESH;
    }

    private void onRefresh(GetTopicsListEvent event) {
        mState = STATE_NORMAL;
        mRefreshLayout.setRefreshing(false);
        mAdapter.clearDatas();
        mAdapter.addDatas(event.getBean());
        mDataCache.saveTopicsList(mAdapter.getDatas());
        toast("数据刷新成功");
    }

    // 加载更多
    private void loadMore() {
        String uuid = mDiycode.getTopicsList(null, null, pageIndex * pageCount, pageCount);
        mPostTypes.put(uuid, POST_LOAD_MORE);
        pageIndex++;
        mState = STATE_LOADING;
        mFooter.setText(FOOTER_LOADING);
    }

    private void onLoadMore(GetTopicsListEvent event) {
        List<Topic> topics = event.getBean();
        if (topics.size() < pageCount) {
            mState = STATE_NO_MORE;
            mFooter.setText(FOOTER_NORMAL);
        } else {
            mState = STATE_NORMAL;
            mFooter.setText(FOOTER_NORMAL);
        }
        mAdapter.addDatas(topics);
        mDataCache.saveTopicsList(mAdapter.getDatas());
        mRefreshLayout.setEnabled(true);
    }

    // 数据加载出现异常
    private void onError(String postType) {
        mState = STATE_NORMAL;  // 状态重置为正常,以便可以重试,否则进入异常状态后无法再变为正常状态
        if (postType.equals(POST_LOAD_MORE)) {
            mFooter.setText(FOOTER_ERROR);
            toast("加载更多失败");
        } else if (postType.equals(POST_REFRESH)) {
            mRefreshLayout.setRefreshing(false);
            toast("刷新数据失败");
        }
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onTopicList(GetTopicsListEvent event) {
        String postType = mPostTypes.get(event.getUUID());
        if (event.isOk()) {
            if (postType.equals(POST_LOAD_MORE)) {
                onLoadMore(event);
            } else if (postType.equals(POST_REFRESH)) {
                onRefresh(event);
            }
        } else {
            onError(postType);
        }
        mPostTypes.remove(event.getUUID());
    }

    @Override
    public void onStart() {
        super.onStart();
        EventBus.getDefault().register(this);
    }

    @Override
    public void onResume() {
        super.onResume();
    }

    @Override
    public void onStop() {
        super.onStop();
        EventBus.getDefault().unregister(this);
    }

    @Override
    public void onDestroyView() {
        // 保存
        int lastScrollY = mScrollView.getScrollY();
        mConfig.saveTopicListScroll(lastScrollY);
        mConfig.saveTopicListPageIndex(pageIndex);
        super.onDestroyView();
    }

    public void quickToTop() {
        if (mScrollView != null) {
            mScrollView.smoothScrollTo(0, 0);
        }
    }
}

如果想要了解我更多的话,可以关注我的微博和网站,详情见下方。

微博: http://weibo.com/GcsSloop

网站: http://www.gcssloop.com

共收到 4 条回复
4108 1491553324

前排支持~~~

96
wxf · #2 ·

高技术

96

MultiTypeAdapter 是借鉴了 drakeet 的 MultiType 吗?代码思想很相像。

1644
sloop · #4 ·

#3楼 @chowaikong 是的,在项目中有说明,不过我写的这一份还有很多问题,后期需要重构一下。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册