【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(三)——使用GreenDao实现本地Sqlite缓存

到目前为止,煎蛋的Android项目算是告一段落了,功能基本都已完成,那么今天,我就介绍一下在煎蛋这个项目里,是怎么完成数据缓存功能的。想看代码的请戳煎蛋项目的GITHUB地址

转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992

  • 缓存功能的解决方案
  • 配置GreenDao
  • 实现缓存功能
  • 其他资料

缓存功能的解决方案

因为算是一个阅读类的应用,所以说如果在无网络情况下,用户打开App还能看到内容的话,属于比较好的用户体验。那么,这就涉及到本地缓存了。

本地缓存有好多解决方案,比如说存数据库里面,或者是存文件里面,甚至可以存在SharedPrefrence里面。我个人更倾向于保存在数据库里面,因为这样进行一些基本操作比较简单。

在煎蛋项目中,缓存数据分成两部分,一部分是请求接口的数据,包括图片url、发布者、段子等等文本类型数据,另外一部分则是图片缓存了。因为UIL已经完成了图片本地缓存功能,所以说,我们只需要缓存请求接口返回的数据就可以了。

既然是缓存在数据库,我们就可以使用Sqlite了,但是直接用Sqlite吧,比较麻烦,那么有木有好用的ORM框架呢?当然有,GreenDao就是比较好的一个ORM框架。因为之前没试过怎么用,就趁着这次机会用用吧,但是真用起来,才发现配置起来确实麻烦,所以下面就介绍下如何使用GreenDao来完成数据库缓存,这应该是最新的GreenDao的使用介绍了。

配置GreenDao

GreenDao使用的时候,需要添加一个辅助项目,来生成数据库的实体类和Dao类。

流程如下:

1. 选中项目

2. 右键

3. new Module

4. 选择类型为Java Library

5. 然后按照下面自己填写,左边是完成的,右边是你要填写的

这样写了之后,我们就有了一个辅助项目了。下面,我们就需要为我们的附注项目添加依赖,所以呢,打开build.gradle文件,然后像下面一样,把我们的依赖库 greendao-generator:1.3.1 添加进去

apply plugin: ‘java‘

repositories {
    mavenLocal()
    mavenCentral()
}

dependencies {
    compile ‘de.greenrobot:greendao-generator:1.3.1‘
}

sourceSets {
    main {
        java {
            srcDir ‘src/main/java‘
        }
    }
}
artifacts {
    archives jar
}

然后就可以在创建的类文件里面,写上下面的代码。当然了,这是煎蛋项目里面的,其他用法你需要自己google:

/**
 * 用来为GreenDao框架生成Dao文件
 */
public class MyDaoGenerator {

    //辅助文件生成的相对路径
    public static final String DAO_PATH = "../app/src/main/java-gen";
    //辅助文件的包名
    public static final String PACKAGE_NAME = "com.socks.greendao";
    //数据库的版本号
    public static final int DATA_VERSION_CODE = 1;

    public static void main(String[] args) throws Exception {

        Schema schema = new Schema(DATA_VERSION_CODE, PACKAGE_NAME);
        addCache(schema, "JokeCache");
        addCache(schema, "FreshNewsCache");
        addCache(schema, "PictureCache");
        addCache(schema, "SisterCache");
        addCache(schema, "VideoCache");
        //生成Dao文件路径
        new DaoGenerator().generateAll(schema, DAO_PATH);

    }

    /**
     * 添加不同的缓存表
     * @param schema
     * @param tableName
     */
    private static void addCache(Schema schema, String tableName) {

        Entity joke = schema.addEntity(tableName);

        //主键id自增长
        joke.addIdProperty().primaryKey().autoincrement();
        //请求结果
        joke.addStringProperty("result");
        //页数
        joke.addIntProperty("page");
        //插入时间,暂时无用
        joke.addLongProperty("time");

    }
}

因为我们需要缓存所有的功能模块,所以呢,调用addCache方法,然后传进去表名就ok啦。

注意在addCache方法里面,我们就四个字段,主键id,接口请求数据result,页码page,添加时间time。因为这几个功能模块的数据很相似,所以这个方法可以复用,如果你需要其他字段,自己使用addXXXProperty()即可。

好了,现在我们的辅助项目就完成了,下面,就需要运行起来,生成辅助文件了。

打开工具栏的这个窗口

点击Edit Configurations,在打开的界面里面,点击左上角的+号,然后选择Application,然后按照下面的提示,把对应位置属性设置好

设置好了,点击OK,然后这时候在工具栏里面,就可以选中我们新创建的项目,然后点击运行,出现下面的提示,就说明我们的辅助实体类和Dao类创建好了。

不信?你打开我们的项目看看,是不是都创建好了~

Ok,到了这一步,我们的任务已经完成50%了。因为找了很多资料,中文英文的都有,要不就是版本太老,要不就是说的不对,就是没有一个成功的,花了好长时间才完成GreenDao的环境搭配,希望这一步对你有所帮助。

实现缓存功能

其实配置好GreenDao之后,后面的工作就是小意思了。

我先说下煎蛋项目里面缓存的思路,当然了,这种实现比较简单,你完全可以扩展。

首先,当有网络连接的时候,我们每次获取新数据的时候,都需要把获取的数据和对应的页码保存进数据库。如果用户执行刷新操作,之前的数据就没用了,直接清除,然后再次把新数据保存起来。当手机是无网络状况的时候,根据页码直接从缓存数据库拿出数据,然后展现出来。对于段子这种纯文本文件,可以让用户查看缓存的文本内容,而对于无聊图这种图片文件,用于有UIL的文件缓存,所以根据我们缓存的url地址,也可以查看图片。

好了,整理了一下思路,下面说一下具体代码实现。

首先,使用GreenDao,我们需要重点关注DaoSession和DaoMaster这两个类,因为我们如果想获取到我们的Dao类,就需要用DaoSession获取。

为了节省资源,官方推荐我们只需要保留一个DaoSession和DaoMaster的实例即可,所以,我们可以直接在Application里面声明称静态常量,来保证一个实例的存在,代码如下:

public class AppAplication extends Application {

    private static Context mContext;

    private static DaoMaster daoMaster;
    private static DaoSession daoSession;

    @Override
    public void onCreate() {
        super.onCreate();
        mContext = this;
        initImageLoader();
        Logger.init().hideThreadInfo();
    }

    public static Context getContext() {
        return mContext;
    }

    /**
     * 初始化ImageLoader
     */
    private void initImageLoader() {
        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this)
                .tasksProcessingOrder(QueueProcessingType.LIFO)
//              .writeDebugLogs()
                .build();
        ImageLoader.getInstance().init(config);
    }

    public static DaoMaster getDaoMaster(Context context) {
        if (daoMaster == null) {
            DaoMaster.OpenHelper helper = new DaoMaster.DevOpenHelper(context, DateBaseInfo.DB_NAME, null);
            daoMaster = new DaoMaster(helper.getWritableDatabase());
        }
        return daoMaster;
    }

    public static DaoSession getDaoSession(Context context) {
        if (daoSession == null) {
            if (daoMaster == null) {
                daoMaster = getDaoMaster(context);
            }
            daoSession = daoMaster.newSession();
        }
        return daoSession;
    }

}

请无视其他无关代码。当然了,如果你需要线程安全,你可以再加个同步锁什么的。

因为所有的缓存逻辑基本相同,所以我抽取了一个Cache的基类BaseCacheUtil,代码如下:

public abstract class BaseCacheUtil<T> {

    protected static DaoSession mDaoSession;

    public abstract void clearAllCache();

    public abstract ArrayList<T> getCacheByPage(int page);

    public abstract void addResultCache(String result, int page);

}

所有子类都必须实现这三个方法,完成数据的清空、添加和获取操作。比如,我们以JokeCacheUtil为例:

public class JokeCacheUtil extends BaseCacheUtil {

    private static JokeCacheUtil instance;
    private static JokeCacheDao mJokeCacheDao;

    private JokeCacheUtil() {
    }

    public static JokeCacheUtil getInstance(Context context) {

        if (instance == null) {

            synchronized (JokeCacheUtil.class) {
                if (instance == null) {
                    instance = new JokeCacheUtil();
                }
            }

            mDaoSession = AppAplication.getDaoSession(context);
            mJokeCacheDao = mDaoSession.getJokeCacheDao();
        }
        return instance;
    }

    /**
     * 清楚全部缓存
     */
    public void clearAllCache() {
        mJokeCacheDao.deleteAll();
    }

    /**
     * 根据页码获取缓存数据
     *
     * @param page
     * @return
     */
    @Override
    public ArrayList<Joke> getCacheByPage(int page) {
        QueryBuilder<JokeCache> query = mJokeCacheDao.queryBuilder().where(JokeCacheDao.Properties.Page.eq("" + page));

        if (query.list().size() > 0) {
            return (ArrayList<Joke>) JSONParser.toObject(query.list().get(0).getResult(),
                    new TypeToken<ArrayList<Joke>>() {
                    }.getType());
        } else {
            return new ArrayList<Joke>();
        }

    }

    /**
     * 添加Jokes缓存
     *
     * @param result
     * @param page
     */
    @Override
    public void addResultCache(String result, int page) {
        JokeCache jokeCache = new JokeCache();
        jokeCache.setResult(result);
        jokeCache.setPage(page);
        jokeCache.setTime(System.currentTimeMillis());

        mJokeCacheDao.insert(jokeCache);
    }

}

在这里使用了线程安全的单例模式。那么我们在代码里面怎么用呢?很简单,首先看我们改造之后的获取方法

public void loadFirst() {
            page = 1;
            loadDataByNetworkType();
        }

        public void loadNextPage() {
            page++;
            loadDataByNetworkType();
        }

        /**
         * 根据不同的网络状态选择不同的加载策略
         */
        private void loadDataByNetworkType() {

            if (NetWorkUtil.isNetWorkConnected(getActivity())) {
                loadData();
            } else {
                loadCache();
            }

        }

之前的获取数据方法,都换成了loadDataByNetworkType(),然后根据网络情况选择不同的加载策略,loadDate()和之前完全一样,loadCache()代码如下

private void loadCache() {

            google_progress.setVisibility(View.GONE);
            mLoadFinisCallBack.loadFinish(null);
            if (mSwipeRefreshLayout.isRefreshing()) {
                mSwipeRefreshLayout.setRefreshing(false);
            }

            JokeCacheUtil jokeCacheUtil = JokeCacheUtil.getInstance(getActivity());
            if (page == 1) {
                mJokes.clear();
                ShowToast.Short(ToastMsg.LOAD_NO_NETWORK);
            }
            mJokes.addAll(jokeCacheUtil.getCacheByPage(page));
            notifyDataSetChanged();

        }

我们从缓存中获取数据,然后添加给适配器,然后刷新即可。

有的同学可能注意到了,那么我们什么时候添加的缓存呀?

因为段子这个功能,需要请求两次接口,第一次是数据,第二次是评论数量,所以我们只能在获取到评论数量之后,再缓存我们的数据,就像下面这样:

private void getCommentCounts(final ArrayList<Joke> jokes) {

            StringBuilder sb = new StringBuilder();
            for (Joke joke : jokes) {
                sb.append("comment-" + joke.getComment_ID() + ",");
            }

            executeRequest(new Request4CommentCounts(CommentNumber.getCommentCountsURL(sb.toString()), new Response
                    .Listener<ArrayList<CommentNumber>>() {

                @Override
                public void onResponse(ArrayList<CommentNumber> response) {

                    google_progress.setVisibility(View.GONE);

                    if (mSwipeRefreshLayout.isRefreshing()) {
                        mSwipeRefreshLayout.setRefreshing(false);
                    }

                    mLoadFinisCallBack.loadFinish(null);

                    for (int i = 0; i < jokes.size(); i++) {
                        jokes.get(i).setComment_counts(response.get(i).getComments() + "");
                    }

                    if (page == 1) {
                        mJokes.clear();
                        //首次正常加载之后,清空之前的缓存
                        JokeCacheUtil.getInstance(getActivity()).clearAllCache();
                    }

                    mJokes.addAll(jokes);
                    notifyDataSetChanged();

                    //加载完毕后缓存
                    JokeCacheUtil.getInstance(getActivity()).addResultCache(JSONParser.toString(jokes),
                            page);

                }
            }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {

                    ShowToast.Short(ToastMsg.LOAD_FAILED);
                    mLoadFinisCallBack.loadFinish(null);
                    google_progress.setVisibility(View.GONE);
                    if (mSwipeRefreshLayout.isRefreshing()) {
                        mSwipeRefreshLayout.setRefreshing(false);
                    }

                }
            }
            ));

        }

还有个问题,就是从数据库解析缓存数据的时候,因为请求的数据,在经过我们解析之后全都转换成了对象,然后我们直接将对象转换成json数据存入的数据库,那么我们再从数据库中把数据拿出来之后,就不能简单的按照之前的解析方法去解析了,有可能需要单独写一个解析方法,比如说新鲜事模块就需要两套解析,而段子模块,因为请求之后的数据直接就是json形式,所以只需要一套即可。

新鲜事的两套解析代码如下

public static ArrayList<FreshNews> parse(JSONArray postsArray) {

        ArrayList<FreshNews> freshNewses = new ArrayList<>();

        for (int i = 0; i < postsArray.length(); i++) {

            FreshNews freshNews = new FreshNews();
            JSONObject jsonObject = postsArray.optJSONObject(i);

            freshNews.setId(jsonObject.optString("id"));
            freshNews.setUrl(jsonObject.optString("url"));
            freshNews.setTitle(jsonObject.optString("title"));
            freshNews.setDate(jsonObject.optString("date"));
            freshNews.setComment_count(jsonObject.optString("comment_count"));
            freshNews.setAuthor(Author.parse(jsonObject.optJSONObject("author")));
            freshNews.setCustomFields(CustomFields.parse(jsonObject.optJSONObject("custom_fields")));
            freshNews.setTags(Tags.parse(jsonObject.optJSONArray("tags")));

            freshNewses.add(freshNews);

        }
        return freshNewses;
    }

    public static ArrayList<FreshNews> parseCache(JSONArray postsArray) {

        ArrayList<FreshNews> freshNewses = new ArrayList<>();

        for (int i = 0; i < postsArray.length(); i++) {

            FreshNews freshNews = new FreshNews();
            JSONObject jsonObject = postsArray.optJSONObject(i);

            freshNews.setId(jsonObject.optString("id"));
            freshNews.setUrl(jsonObject.optString("url"));
            freshNews.setTitle(jsonObject.optString("title"));
            freshNews.setDate(jsonObject.optString("date"));
            freshNews.setComment_count(jsonObject.optString("comment_count"));
            freshNews.setAuthor(Author.parse(jsonObject.optJSONObject("author")));
            freshNews.setCustomFields(CustomFields.parseCache(jsonObject.optJSONObject("custom_fields")));
            freshNews.setTags(Tags.parseCache(jsonObject.optJSONObject("tags")));

            freshNewses.add(freshNews);

        }
        return freshNewses;
    }

其他的代码都很相似了,虽然说起来很简单,但是在做的时候,在这个坑里耽误了一些时间,以此为戒。

其他资料

GreenDao官网

时间: 2024-10-17 03:25:13

【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(三)——使用GreenDao实现本地Sqlite缓存的相关文章

【凯子哥带你夯实应用层】都说“知乎”逼格高,我们来实现“知乎”回答详情页动画效果

转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992 2014已经远去,2015年的目标很简单,就是继续熟悉Android的上层API,虽然偶尔会为了某个问题去研究下FrameWork的代码,但是对于我们这种新手来说,只有对上层的API用的熟练了,才能更好的往下研究原理.所以,今年的任务就是继续学习和研究Android的上层API的使用,顺便写一篇毕业论文,然后毕个业. OK,从这篇开始,咱们就开始[凯子哥带你夯实应用层]系列,如果你有想要实现的界面效果,或

【凯子哥带你夯实应用层】滚来滚去,滚来滚去...Scroller相关类使用大揭秘!!!

转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992 话接上文,在前一篇文章里面,咱们一起分析了"知乎"的回答详情页的需求,然后顺便用代码实现了下,忘了的可以再去看看[凯子哥带你夯实应用层]都说"知乎"逼格高,我们来实现"知乎"回答详情页动画效果 .其实在很多的界面效果中,这种"滚动"的效果能带来很多的惊喜,各种效果也很有搞头,说不定什么时候,Boss看着哪个界面好看,就让你去仿个过来,你

【转载】【凯子哥带你学Framework】Activity启动过程全解析

It's right time to learn Android's Framework ! 前言 一个App是怎么启动起来的? App的程序入口到底是哪里? Launcher到底是什么神奇的东西? 听说还有个AMS的东西,它是做什么的? Binder是什么?他是如何进行IPC通信的? Activity生命周期到底是什么时候调用的?被谁调用的? 等等... 你是不是还有很多类似的疑问一直没有解决?没关系,这篇文章将结合源码以及大量的优秀文章,站在巨人的肩膀上,更加通俗的来试着解释一些问题.但是毕

【凯子哥带你学Framework】Activity启动过程全解析

It's right time to learn Android's Framework ! 前言 学习目标 写作方式 主要对象功能介绍 主要流程介绍 zygote是什么有什么作用 SystemServer是什么有什么作用它与zygote的关系是什么 ActivityManagerService是什么什么时候初始化的有什么作用 Launcher是什么什么时候启动的 Instrumentation是什么和ActivityThread是什么关系 如何理解AMS和ActivityThread之间的Bi

【凯子哥带你学Android】Andriod性能优化之列表卡顿——以“简书”APP为例

这几天闲得无聊,就打开手机上的开发者模式里面的"GPU过度绘制"功能,看看别家的App做的咋样,然后很偶然的打开了"简书",然后就被它的过度绘制惊呆了,于是写了这篇性能分析的文章,从一个只有APK文件的角度,说下如何寻找布局中可能存在的性能问题,以及解决方案.本文章以简书Android最新版本1.9.1进行分析. GPU过度绘制 Hierarchy View SysTrace TraceView 总结 分析资源下载 GPU过度绘制 首先打开下面两个功能开关 开发者模

【凯子哥带你夯实应用层】还在用XListView?试试更漂亮的AutoLoadListView吧!

转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992 关于上拉刷新和下拉加载,已经有不少的解决方案了,XListView和PullToRefresh应该是被广为熟知的开源项目,项目很棒,可以解决我们的需求.但是,今天咱们用一种更简单的方式,来实现上拉刷新和下拉加载功能,我叫它AutoLoadListView~ 先来一张效果图. 刷新效果使用19版本之后的v4兼容包的SwipeRefreshLayout实现,效果很漂亮,而自动加载下一页的跳动效果,则是使用了另

【凯子哥带你学Framework】Activity界面显示全解析

前几天凯子哥写的Framework层的解析文章<Activity启动过程全解析>,反响还不错,这说明“写让大家都能看懂的Framework解析文章”的思想是基本正确的. 我个人觉得,深入分析的文章必不可少,但是对于更多的Android开发者——即只想做应用层开发,不想了解底层实现细节——来说,“整体上把握,重要环节深入“是更好的学习方式.因为这样既可以有完整的知识体系,又不会在浩瀚的源码世界里迷失兴趣和方向. 所以呢,今天凯子哥又带来一篇文章,接着上一篇的结尾,重点介绍Activity开启之后

【转载】【凯子哥带你学Framework】Activity界面显示全解析(下)

如何验证上一个问题 首先,说明一下运行条件 //主题 name="AppTheme" parent="@android:style/Theme.Holo.Light.NoActionBar" //编译版本 android { compileSdkVersion 19 buildToolsVersion '19.1.0' defaultConfig { applicationId "com.socks.uitestapp" minSdkVersio

【凯子哥带你学Android】Android专用Log开源项目——KLog

在Android开发和调试的过程中,Log的使用是非常频繁的,一个好的Log工具可以帮你节省很多时间,所以凯子哥抽空写了个这个开源项目KLog,希望可以帮助大家提高开发效率,本开源库的灵感来自于Logger KLog的特点 运行演示 使用详解 KLogd KLogdString KLogdTagString KLogjsonString KLogjsonTagString 注意事项 使用JCenter引用 为什么叫KLog 如何设置Log的颜色 项目地址 KLog的特点 支持显示行号 支持显示L