Android之数据存储----使用LoaderManager异步加载数据库

简介:

一、各种概念:

1、Loaders:

适用于Android3.0以及更高的版本,它提供了一套在UI的主线程中异步加载数据的框架。使用Loaders可以非常简单的在Activity或者Fragment中异步加载数据,一般适用于大量的数据查询,或者需要经常修改并及时展示的数据显示到UI上,这样可以避免查询数据的时候,造成UI主线程的卡顿。

即使是查询SQLite数据库,用Loaders来操作会更加的简便。

Loaders有以下特点:

  • 可以适用于Activity和Fragment。
  • 可以提供异步的方式加载数据。
  • 监听数据源,当数据改变的时候,将新的数据发布到UI上。
  • Loaders使用Cursor加载数据,在更改Cursor的时候,会自动重新连接到最后配置的Cursor中读取数据,因此不需要重新查询数据。

在Android中使用Loaders机制,需要多个类和接口的配合,以下是它们大致的关系图,之后的内容会对这几个类或接口进行详细讲解:

2、LoaderManager

用于在Activity或者Fragment中管理一个或多个Loader实例。在Activity或者Fragment中,可以通过getLoaderManager()方法获取LoaderManager对象,它是一个单例模式。

  • 介绍几个LoaderManager提供的方法,用于管理Loader:
  • Loader<D> initLoader(int id,Bundle bundle,LoaderCallbacks<D> callback):初始化一个Loader,并注册回调事件。
  • Loader<D> restartLoader(int id,Bundle bundle,LoaderCallbacks<D> callback):重新启动或创建一个Loader,并注册回调事件。
  • Loader<D> getLoader(int id):返回给定Id的Loader,如果没有找到则返回Null。
  • void destroyLoader(int id):根据指定Id,停止和删除Loader。

通过上面几个方法的参数可以看到,都有一个id参数,这个Id是Loader的标识,因为LoaderManager可以管理一个或多个Loader,所以必须通过这个Id参数来唯一确定一个Loader。而InitLoader()、restartLoader()中的bundle参数,传递一个Bundle对象给LoaderCallbacks中的onCreateLoader()去获取,下面介绍LoaderCallbacks。

3、LoaderManager.LoaderCallbacks

LoaderCallbacks是LoaderManager和Loader之间的回调接口。它是一个回调接口,所以我们需要实现其定义的三个方法:

  • Loader<D> onCreateLoader(int id,Bundle bundle):根据指定Id,初始化一个新的Loader,并返回。
  • void onLoadFinished(Loader<D> loader,D data):当Loader被加载完毕后被调用,在其中处理Loader获取的Cursor数据。
  • void onLoaderReset(Loader<D> loader):当Loader被销毁的时候被调用,在其中可以使Loader的数据不可用。

从LoaderCallbacks的声明的几个方法中可以看到,它是一个泛型的接口,需要指定Loader数据的类型。如果是数据源是从一个ContentProvider中获取的,一般直接使用它的子类CursorLoader,下面介绍CursorLoader。

4、CursorLoader

我们知道,Loader一个抽象的类,用于执行异步加载数据,这个Loader对象可以监视数据源的改变和在内容改变后,以新数据的内容改变UI的展示。它是一个抽象接口,所有需要实现的Loader功能的类都需要实现这个接口,但是如果需要自己开发一个装载机的话,一般并不推荐继承Loader接口,而是继承它的子类AsyncTaskLoader,这是一个以AsyncTask框架执行的异步加载。

Android中还提供了一个CursorLoader类,它是AsyncTaskLoader的子类,一个异步的加载数据的类,通过ContentResolver的标准查询并返回一个Cursor。这个类实现了Loader的协议,以一种标准的方式查询Cursor。

CursorLoader类有两个构造函数,推荐使用第二个,因为使用第一个构造函数,需要还需要通过CursorLoader提供的一些了getXXX()方法设置对应的属性。两个构造方法如下:

  • CursorLoader(Context context)
  • CursorLoader(Context context,Uri uri,String[] projection,String selection ,String[] selectionArgs,String sortOrder)

 

二、代码举例:

在这个例子中,数据使用SQLite数据库保存,然后用ContentProvider进行数据的请求与访问。在SQLite数据库中,已经存在一个Student表,它有两个字段:_id,name。在本例中,使用一个ListView展示数据,使用LoaderManager管理一个Loader,并通过这个Loader的回调接口进行加载ListView的数据显示并实时刷新,最终进行完成对SQLite数据库中的数据进行增加与删除。

整个工程文件的目录结构如下:

具体步骤如下:

步骤(1):新建类PersonDao,用于进行对SQLite的CRUD操作

步骤(2):新建类DBHelper,用于初始化SQLiate数据库

步骤(3):新建类PersonContentProvider,继承ContetProvider,记得声明权限。

步骤(4):添加单元测试类。我们在单元测试里向SQLite中添加一些记录。

注:上述步骤是ContentProvider中的知识,和上一篇博文:ContentProvider内容提供者中的步骤一模一样。所以这里就不列出代码了,如果不明白的话,可以回去回顾一下,或者在本文的最后下载源码也行。

我们在步骤(4)的单元测试里向数据库中添加一些数据之后,可以开始接下来最关键的步骤了:

步骤(5):

MainActivity.java:

复制代码
  1 package com.example.loadermanagertest;
  2 
  3 import android.annotation.SuppressLint;
  4 import android.app.Activity;
  5 import android.app.AlertDialog;
  6 import android.app.LoaderManager;
  7 import android.app.LoaderManager.LoaderCallbacks;
  8 import android.content.ContentResolver;
  9 import android.content.ContentValues;
 10 import android.content.CursorLoader;
 11 import android.content.Loader;
 12 import android.database.Cursor;
 13 import android.net.Uri;
 14 import android.os.Bundle;
 15 import android.util.Log;
 16 import android.view.ContextMenu;
 17 import android.view.ContextMenu.ContextMenuInfo;
 18 import android.view.LayoutInflater;
 19 import android.view.Menu;
 20 import android.view.MenuInflater;
 21 import android.view.MenuItem;
 22 import android.view.View;
 23 import android.widget.AdapterView.AdapterContextMenuInfo;
 24 import android.widget.Button;
 25 import android.widget.EditText;
 26 import android.widget.ListView;
 27 import android.widget.SimpleCursorAdapter;
 28 import android.widget.TextView;
 29 
 30 @SuppressLint("InflateParams") public class MainActivity extends Activity {
 31     private LoaderManager manager;
 32     private ListView listview;
 33     private AlertDialog alertDialog;
 34     private SimpleCursorAdapter mAdapter;
 35     private final String TAG="MainActivity";
 36 
 37     @Override
 38     protected void onCreate(Bundle savedInstanceState) {
 39         super.onCreate(savedInstanceState);
 40         setContentView(R.layout.activity_main);
 41         listview = (ListView) findViewById(R.id.listView1);
 42         //使用一个SimpleCursorAdapter,布局使用android自带的布局资源simple_list_item_1, android.R.id.text1 为simple_list_item_1中TextView的Id
 43         mAdapter = new SimpleCursorAdapter(MainActivity.this,
 44                 android.R.layout.simple_list_item_1, null,
 45                 new String[] { "name" }, new int[] { android.R.id.text1 },0);
 46         
 47         // 获取Loader管理器。
 48         manager = getLoaderManager();
 49         // 初始化并启动一个Loader,设定标识为1000,并制定一个回调函数。
 50         manager.initLoader(1000, null, callbacks);
 51 
 52         // 为ListView注册一个上下文菜单
 53         registerForContextMenu(listview);
 54     }
 55 
 56     @Override
 57     public void onCreateContextMenu(ContextMenu menu, View v,
 58             ContextMenuInfo menuInfo) {
 59         super.onCreateContextMenu(menu, v, menuInfo);
 60         // 声明一个上下文菜单,contentmenu中声明了两个菜单,添加和删除
 61         MenuInflater inflater = getMenuInflater();
 62         inflater.inflate(R.menu.contentmenu, menu);
 63     }
 64 
 65     //单击单个的item,弹出菜单选项,让你选择是添加还是删除
 66     @Override
 67     public boolean onContextItemSelected(MenuItem item) {
 68         
 69         switch (item.getItemId()) {
 70         //当用户点击菜单中的“添加”选项是,弹出对话框,在对话框里添加name的值
 71         case R.id.menu_add:
 72             // 添加一个对话框
 73             AlertDialog.Builder builder = new AlertDialog.Builder(
 74                     MainActivity.this);
 75             // 加载一个自定义布局,add_name中有一个EditText和Button控件。
 76             final View view = LayoutInflater.from(MainActivity.this).inflate(
 77                     R.layout.add_name, null);
 78             Button btnAdd = (Button) view.findViewById(R.id.btnAdd);
 79             btnAdd.setOnClickListener(new View.OnClickListener() {
 80 
 81                 @Override
 82                 public void onClick(View v) {
 83                     EditText etAdd = (EditText) view
 84                             .findViewById(R.id.username);
 85                     String name = etAdd.getText().toString();
 86                     // 使用ContentResolver进行删除操作,根据name字段。
 87                     ContentResolver contentResolver = getContentResolver();
 88                     ContentValues contentValues = new ContentValues();
 89                     contentValues.put("name", name);
 90                     Uri uri = Uri
 91                             .parse("content://com.example.loadermanagertest.PersonContentProvider/person");
 92                     Uri result = contentResolver.insert(uri, contentValues);
 93                     if (result != null) {
 94                         //result不为空证明添加成功,重新启动Loader,注意标识需要和之前init的标识一致。
 95                         manager.restartLoader(1000, null, callbacks);
 96                     }
 97                     // 关闭对话框
 98                     alertDialog.dismiss();
 99                     
100                     Log.i(TAG, "--->>添加数据成功,name="+name);
101                 }
102             });
103             builder.setView(view);
104             alertDialog = builder.show();
105             return true;
106         case R.id.menu_delete:
107             // 获取菜单选项的信息
108             AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
109                     .getMenuInfo();
110             // 获取到选项的TextView控件,并得到选中项的内容
111             TextView tv = (TextView) info.targetView;
112             String name = tv.getText().toString();
113             // 使用ContentResolver进行删除操作
114             Uri url = Uri
115                     .parse("content://com.example.loadermanagertest.PersonContentProvider/person");
116             ContentResolver contentResolver = getContentResolver();
117             String where = "name=?";
118             String[] selectionArgs = { name };
119             int count = contentResolver.delete(url, where, selectionArgs);
120             if (count == 1) {
121                 //这个操作仅删除单条记录,如果删除行为1 ,则重新启动Loader
122                 manager.restartLoader(1000, null, callbacks);
123             }
124             Log.i(TAG, "--->>删除数据成功,name="+name);
125             return true;
126         default:
127             return super.onContextItemSelected(item);
128         }
129 
130     }
131 
132     // Loader的回调接口,在这里异步加载数据库的内容,显示在ListView上,同时能够自动更新
133     private LoaderManager.LoaderCallbacks<Cursor> callbacks = new LoaderCallbacks<Cursor>() {
134 
135         @Override
136         public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
137             // 在Loader创建的时候被调用,这里使用一个ContentProvider获取数据,所以使用CursorLoader返回数据
138             Uri uri = Uri
139                     .parse("content://com.example.loadermanagertest.PersonContentProvider/person");
140             CursorLoader loader = new CursorLoader(MainActivity.this, uri,
141                     null, null, null, null);
142             Log.i(TAG, "--->>onCreateLoader被执行。");
143             return loader;
144         }
145 
146         //完成对UI的数据提取,更新UI
147         @Override
148         public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
149             //把数据提取出来,放到适配器中完成对UI的更新操作(刷新SimpleCursorAdapter的数据)
150             mAdapter.swapCursor(cursor);
151             // 为ListView绑定适配器
152             listview.setAdapter(mAdapter);
153             Log.i(TAG, "--->>onLoadFinished被执行。");
154         }
155 
156         @Override
157         public void onLoaderReset(Loader<Cursor> loader) {
158             // 当Loader被从LoaderManager中移除的时候,被执行,清空SimpleCursorAdapter适配器的Cursor
159             mAdapter.swapCursor(null);
160             Log.i(TAG, "--->>onLoaderReset被执行。");
161         }
162     };
163 
164     @Override
165     public boolean onCreateOptionsMenu(Menu menu) {
166         getMenuInflater().inflate(R.menu.main, menu);
167         return true;
168     }
169 
170 }
复制代码

核心代码:132行至162行、95行和122行的实时刷新

注意为ListView绑定适配器的代码:listview.setAdapter(mAdapter)是在Loader的回调接口中(132行)进行的,也就是在这里更新UI,这样就能够实现自动刷新UI。

43行:新建一个SimpleCursorAdapter适配器

先通过getLoaderManager()方法获取LoaderManager对象(48行),然后通过manager.initLoader(1000, null, callbacks)初始化一个Loader(50行)。其方法的完整版是:

public abstract <D> Loader<D> initLoader(int id,Bundle args, LoaderManager.LoaderCallbacks<D> callback)

  • 第一个参数id:一个Acticity中可以加载多个Loader,所以要给每个Loader制定一个唯一的标识符id。第二个参数可以置空。
  • 第三个参数callback:回调。

Loader的回调接口是在132行至162行定义的,也就是在这里异步加载数据库的内容,显示在ListView上,同时能够自动更新

我们在第53行为ListView注册一个上下文菜单,上下文的菜单布局是在62行的R.menu.contentmenu.xml中定义的(稍后给出代码)。

当用户单击单个的item时,弹出菜单选项,让你选择是添加还是删除(67行定义的方法)。如果是选择添加内容,则弹出一个对话框(71行)(对话框的布局文件R.layout.add_name稍后给出),输入需要加入的内容,单击确定,就会更新到UI(95行的manager.restartLoader(1000, null, callbacks)方法);如果选择删除内容,则直接删除,并更新UI(122行的manager.restartLoader(1000, null, callbacks)方法)。

R.menu.contentmenu.xml:用于定义上下文菜单的布局

复制代码
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/menu_add"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="添加">
    </item>
    <item
        android:id="@+id/menu_delete"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="删除">
    </item>
</menu>
复制代码

R.layout.add_name.xml:定义添加内容的布局

复制代码
 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3     android:layout_width="240dp"
 4     android:layout_height="wrap_content"
 5     android:orientation="vertical" >
 6 
 7     <TextView
 8         android:id="@+id/textView1"
 9         android:layout_width="wrap_content"
10         android:layout_height="wrap_content"
11         android:text="姓名:" />
12 
13     <EditText
14         android:id="@+id/username"
15         android:layout_width="match_parent"
16         android:layout_height="wrap_content"
17         android:layout_marginBottom="4dp"
18         android:layout_marginLeft="4dp"
19         android:layout_marginRight="4dp"
20         android:layout_marginTop="16dp"
21         android:hint="username"
22         android:inputType="textEmailAddress" />
23 
24     <Button
25         android:id="@+id/btnAdd"
26         android:layout_width="match_parent"
27         android:layout_height="wrap_content" android:text="确定">
28     </Button>
29 
30 </LinearLayout>
复制代码

布局效果如下:

8ead0ac2-df0c-4efd-9403-5da9f016959e

运行程序,动态演示效果如下:

 

图文分解如下:

初始界面为:

71bab64b-aafe-445d-903d-8bf9c2feae7d

单击长按第二个item,会弹出一个菜单:

0b7707d4-c959-44e6-8676-b4c45f25d342

如果我们选择上图菜单中的“添加”,会弹出一个对话框:

831b74ca-9828-450f-9ce9-fb75b07e16f9

我们在上面的对话框中输入smyhvae,点击“确定”,就会将内容添加到ListView中,并自动更新UI:

40974acd-1a9b-4ad6-a782-c23c477ec643

如果单击菜单中的“删除”,会直接删除。

注:本文采用的适配器是SimpleCursorAdapter,可以参考老罗的老版视频,采用的是自定义适配器BaseAdapter。

相关文章
|
17天前
|
关系型数据库 MySQL 数据库
RDS MySQL灾备服务协同解决方案构建问题之数据库备份数据的云上云下迁移如何解决
RDS MySQL灾备服务协同解决方案构建问题之数据库备份数据的云上云下迁移如何解决
|
18天前
|
存储 消息中间件 人工智能
AI大模型独角兽 MiniMax 基于阿里云数据库 SelectDB 版内核 Apache Doris 升级日志系统,PB 数据秒级查询响应
早期 MiniMax 基于 Grafana Loki 构建了日志系统,在资源消耗、写入性能及系统稳定性上都面临巨大的挑战。为此 MiniMax 开始寻找全新的日志系统方案,并基于阿里云数据库 SelectDB 版内核 Apache Doris 升级了日志系统,新系统已接入 MiniMax 内部所有业务线日志数据,数据规模为 PB 级, 整体可用性达到 99.9% 以上,10 亿级日志数据的检索速度可实现秒级响应。
AI大模型独角兽 MiniMax 基于阿里云数据库 SelectDB 版内核 Apache Doris 升级日志系统,PB 数据秒级查询响应
|
19天前
|
JSON 前端开发 Java
数据库中的时间和前台展示的时间不一样,如何保存日期格式的数据到数据库? 如何展示数据库的日期数据到前台
这篇文章讨论了前端展示时间和数据库时间不一致的问题,并提供了解决方法,包括在SpringBoot的`application.properties`中配置时区和日期格式,以及如何将日期数据格式化后保存到数据库中。
数据库中的时间和前台展示的时间不一样,如何保存日期格式的数据到数据库? 如何展示数据库的日期数据到前台
|
9天前
|
SQL 存储 数据处理
"SQL触发器实战大揭秘:一键解锁数据自动化校验与更新魔法,让数据库管理从此告别繁琐,精准高效不再是梦!"
【8月更文挑战第31天】在数据库管理中,确保数据准确性和一致性至关重要。SQL触发器能自动执行数据校验与更新,显著提升工作效率。本文通过一个员工信息表的例子,详细介绍了如何利用触发器自动设定和校验薪资,确保其符合业务规则。提供的示例代码展示了在插入新记录时如何自动检查并调整薪资,以满足最低标准。这不仅减轻了数据库管理员的负担,还提高了数据处理的准确性和效率。触发器虽强大,但也需谨慎使用,以避免复杂性和性能问题。
20 1
|
9天前
|
安全 关系型数据库 数据库
FastAPI数据库操作秘籍:如何通过高效且安全的数据库访问策略,使你的Web应用飞速运转并保持数据完整性?
【8月更文挑战第31天】在构建现代Web应用时,数据库操作至关重要。FastAPI不仅简化了API创建,还提供了高效数据库交互的方法。本文探讨如何在FastAPI中实现快速、安全的数据处理。FastAPI支持多种数据库,如SQLite、PostgreSQL和MySQL;选择合适的数据库可显著提升性能。通过安装相应驱动并配置连接参数,结合ORM库(如Tortoise-ORM或SQLAlchemy),可以简化数据库操作。使用索引、批量操作及异步处理等最佳实践可进一步提高效率。同时,确保使用参数化查询防止SQL注入,并从环境变量中读取敏感信息以增强安全性。
14 1
|
14天前
|
缓存 运维 监控
打造稳定高效的数据引擎:数据库服务器运维最佳实践全解析
打造稳定高效的数据引擎:数据库服务器运维最佳实践全解析
|
16天前
|
运维 安全 Cloud Native
核心系统转型问题之分布式数据库和数据访问中间件协作如何解决
核心系统转型问题之分布式数据库和数据访问中间件协作如何解决
|
18天前
|
安全 Java 关系型数据库
毕设项目&课程设计&毕设项目:基于springboot+jsp实现的健身房管理系统(含教程&源码&数据库数据)
本文介绍了一款基于Spring Boot和JSP技术实现的健身房管理系统。随着健康生活观念的普及,健身房成为日常锻炼的重要场所,高效管理会员信息、课程安排等变得尤为重要。该系统旨在通过简洁的操作界面帮助管理者轻松处理日常运营挑战。技术栈包括:JDK 1.8、Maven 3.6、MySQL 8.0、JSP、Shiro、Spring Boot 2.0等。系统功能覆盖登录、会员管理(如会员列表、充值管理)、教练管理、课程管理、器材管理、物品遗失管理、商品管理及信息统计等多方面。
|
15天前
|
存储 SQL JSON
【Azure Logic App】微软云逻辑应用连接到数据库,执行存储过程并转换执行结果为JSON数据
【Azure Logic App】微软云逻辑应用连接到数据库,执行存储过程并转换执行结果为JSON数据
【Azure Logic App】微软云逻辑应用连接到数据库,执行存储过程并转换执行结果为JSON数据