文章目录
概述
综合ListView、SQLite、网络操作来完成一下需求:
- 页显示内容:广告图片、文章标题、书本、内容
- 点击【上一篇】显示上一篇文章(实时修改页面文章标题、作者、内容,图片不变),到了最前面直接显示最后一篇。点击【下一篇】显示下一篇文章,到了最后一篇再点击显示第一批(一共30篇)
- 点击【收藏】按钮,如果该文章已被收藏,则不进行重复收藏并给出相关提示。若未被收藏过并此次收藏成功,则提示“收藏成功”.
- 点击【我的收藏】进入新界面,显示所有被收藏的文章标题列表,点击列表,进入详情显示。
运行效果
分析
需要涉及到网络请求,先完成网络权限的配置。功能分为以下几块来实现:
Book: 书本类
BookDbHelper: 数据库辅助类,创建数据库,并使用单例模式创建数据库对象
BookDao: 用于向外公布对Book数据库的操作方法
BookBiz: 书本业务类,获取、解析书本网络数据
ImageFile: 图片文件操作类,向外公布对图片对象的下载及存储方法
ImageBiz: 图片业务类,获取、解析图片网络数据
三个Activity
MainActivity: 程序主界面,显示广告图片、文章标题、作者、内容
MyCollectionActivity: 我的收藏,显示收藏的书本信息
ReadActivity: 阅读界面,显示书本标题、作者和内容
使用到的链接:
广告图片
http://www.imooc.com/api/teacher?type=1
文章
参数(cid=?):书本id:可选数字1~30
http://www.imooc.com/api/teacher?type=3&cid=1
代码实现
因为涉及到网络操作,所以先在manifest文件里完成相关的配置,同时图片的加载我希望是只在第一次运行的时候从网络获取,然后保存到本地,第二次运行可以直接从本地获取,所以在manifest文件中也要完成对外部存储权限的申请
<!--外部存储权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Book
此类声明了_id、title、author、content
四个String
类型的私有属性,同时也声明了五个跟数据库列名有关的常量
因为_id
这个属性我希望在数据库中它是自增的,实例化Book对象的时候并不需要赋值,所以这里声明了三个参数的构造方法
public class Book {
private String _id,title,author,content;
public static final String TABLE_NAME = "tb_book";
public static final String COL_ID = "_id";
public static final String COL_TITLE = "title";
public static final String COL_AUTHOR = "author";
public static final String COL_CONTENT = "content";
public Book() {
}
public Book(String title, String author, String content) {
this.title = title;
this.author = author;
this.content = content;
}
public String get_id() {
return _id;
}
public void set_id(String _id) {
this._id = _id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static String getTableName() {
return TABLE_NAME;
}
public static String getColId() {
return COL_ID;
}
public static String getColTitle() {
return COL_TITLE;
}
public static String getColAuthor() {
return COL_AUTHOR;
}
public static String getColContent() {
return COL_CONTENT;
}
}
BookDbHelper
数据库辅助类,继承了SQLiteOpenHelper,实现创建数据库,并使用单例模式创建数据库对象
因为整个程序运行过程中有且仅有一个书本数据库,所以将它设为单例模式,也方便后续操作
创建数据表的时候可以使用Book类里声明的变量
public class BookDbHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "db_book.db";
private static final int VERSION = 1;
// 使用懒汉式单例模式,只存在一个BookDbHelper
private static BookDbHelper bookDbHelper;
public static synchronized BookDbHelper getInstance(Context context) {
if (bookDbHelper == null) {
bookDbHelper = new BookDbHelper(context.getApplicationContext());
}
return bookDbHelper;
}
// 构造函数
public BookDbHelper(@Nullable Context context) {
super(context, DB_NAME, null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// 数据库表格创建
db.execSQL("CREATE TABLE IF NOT EXISTS " + Book.TABLE_NAME
+ "("
+ Book.COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ Book.COL_TITLE + " VARCHAR,"
+ Book.COL_AUTHOR + " VARCHAR,"
+ Book.COL_CONTENT + " VARCHAR"
+ ")");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
BookDao
用于对外公布对Book数据库的操作方法:
loadFromDb: 从数据库中获取数据
insert2Db: 将数据存入数据库
public class BookDao {
// 从数据库中获取
public List<Book> loadFromDb(Context context) {
BookDbHelper dbHelper = BookDbHelper.getInstance(context);
SQLiteDatabase db = dbHelper.getReadableDatabase();
List<Book> bookList = new ArrayList<>();
Cursor cursor = db.rawQuery("SELECT * FROM " + Book.TABLE_NAME, null);
Book book = null;
while(cursor.moveToNext()){
String id = cursor.getString(cursor.getColumnIndex(Book.COL_ID));
String title = cursor.getString(cursor.getColumnIndex(Book.COL_TITLE));
String author = cursor.getString(cursor.getColumnIndex(Book.COL_AUTHOR));
String content = cursor.getString(cursor.getColumnIndex(Book.COL_CONTENT));
book = new Book(title,author,content);
bookList.add(book);
}
cursor.close();
return bookList;
}
// 缓存入数据库
public void insert2Db(Context context, List<Book> bookList) {
// 校验传入数据
if (context == null) {
// context不能为空
throw new IllegalArgumentException("context can not be null.");
}
if (bookList == null || bookList.isEmpty()) {
return;
}
BookDbHelper dbHelper = BookDbHelper.getInstance(context);
SQLiteDatabase db = dbHelper.getWritableDatabase();
db.beginTransaction();
ContentValues contentValues = null;
for (Book book : bookList) {
contentValues = new ContentValues();
contentValues.put(Book.COL_ID, book.get_id());
contentValues.put(Book.COL_TITLE, book.getTitle());
contentValues.put(Book.COL_AUTHOR, book.getAuthor());
contentValues.put(Book.COL_CONTENT, book.getContent());
db.insertWithOnConflict(Book.TABLE_NAME,
null,
contentValues,
SQLiteDatabase.CONFLICT_REPLACE);
}
db.setTransactionSuccessful();
db.endTransaction();
}
}
本身没有相关需求,这里省略了删除和修改操作
BookBiz
书本业务类,通过异步方式获取、解析书本网络数据,并根据获取情况通过一个回调接口来返回相应对象
public class BookBiz {
private BookDao bookDao = new BookDao();
// 获取网络数据
// 参数1:环境上下文 参数2:根据获取情况返回相应对象 参数3:是否使用缓存
public void loadBookDatas(final Context context, final CallBack callBack, final boolean useCache) {
// 通过异步类获取网络数据
AsyncTask<Boolean, Void, List<Book>> asyncTask = new AsyncTask<Boolean, Void, List<Book>>() {
private Exception e;
@Override
protected List<Book> doInBackground(Boolean... booleans) {
boolean isUseCache = booleans[0];
List<Book> bookList = new ArrayList<>();
try {
if (isUseCache) {
// 从db中获取数据
bookList.addAll(bookDao.loadFromDb(context));
}
// isUseCache为false或者无法从缓存获取数据,bookList都是空的
// 当bookList为空时,直接从网络获取数据
if (bookList.isEmpty()) {
List<Book> bookListFromNet = loadFromNet(context);
bookList.addAll(bookListFromNet);
// 缓存至db
bookDao.insert2Db(context, bookListFromNet);
}
} catch (Exception e) {
e.printStackTrace();
this.e = e;
}
return bookList;
}
@Override
protected void onPostExecute(List<Book> books) {
super.onPostExecute(books);
// 根据请求数据的情况调用接口来传递数据
if (e != null) {
callBack.onFailed(e);
return;
}
callBack.onSuccess(books);
}
};
asyncTask.execute(useCache);
}
// 从网络获取数据(三十个)
private List<Book> loadFromNet(Context context) {
List<Book> bookList = new ArrayList<>();
for (int i = 1; i <= 30; i++) {
String urlBook = "http://www.imooc.com/api/teacher?type=3&cid="+i;
// 发请求获取json数据
String jsonstr = HttpUtils.doGet(urlBook).toString();
// 当jsonstr不为空时,解析json数据
if (jsonstr != null) {
bookList.add(parseJsonstr(jsonstr));
}
}
return bookList;
}
// 解析数据
private Book parseJsonstr(String jsonstr) {
try {
JSONObject root = new JSONObject(jsonstr);
String msgstr = root.optString("msg");
if (msgstr.equals("成功")) {
root = root.getJSONObject("data");
String title = root.getString("title");
String author = root.getString("author");
String content = root.getString("content");
Book book = new Book(title,author,content);
return book;
}
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
// 返回接口
public static interface CallBack {
void onSuccess(List<Book> bookList);
void onFailed(Exception e);
}
}
ImageFile
图片文件操作类,向外公布对图片对象的下载及存储方法
这里存储图片对象优先选择外部存储,存储在手机相册中,如果外部存储不可用再使用内部存储
loadFromFile: 从外部存储(手机相册)或内部存储中读取图片对象
Insert2File: 将图片通过字节数组以JPG格式存储在外部存储(手机相册)或内部存储中
public class ImageFile {
private File f;
// 从外部存储中读取图片对象
public Bitmap loadFromFile(Context context) {
// 判断外部存储是否可用,不可用则使用内部存储
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
if (f == null) {
f = new File(context.getExternalFilesDir(Environment.DIRECTORY_DCIM), "image.jpg");
}
}else{
f = new File(context.getFilesDir(),"img.jpg");
}
// 若文件存在则读取,并转换成Bitmap对象
if (f.exists()) {
Bitmap bm = BitmapFactory.decodeFile(f.toString(), new BitmapFactory.Options());
return bm;
}
return null;
}
// 将字节数组以JPG格式存储到外部存储中
public void Insert2File(Context context, Bitmap bitmap) {
// 判断外部存储是否可用,不可用则使用内部存储
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
if (f == null) {
f = new File(context.getExternalFilesDir(Environment.DIRECTORY_DCIM), "image.jpg");
}
}else{
f = new File(context.getFilesDir(),"img.jpg");
}
// 转换为字节流存储
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] bytes = baos.toByteArray();
if (!f.exists()) {
try {
f.createNewFile();
FileOutputStream fos = new FileOutputStream(f);
fos.write(bytes);
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ImageBiz
图片业务类,通过异步方式获取、解析图片网络数据,并设置控件显示
public class ImageBiz {
private ImageFile imageFile = new ImageFile();
public void loadImageDatas(final Context context, Boolean useCache) {
// 通过异步类获取网络图片
AsyncTask<Boolean, Void, Bitmap> asyncTask = new AsyncTask<Boolean, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(Boolean... booleans) {
Boolean isUseCache = booleans[0];
Bitmap bm = null;
if (isUseCache) {
// 在本地中获取Bitmap对象
bm = imageFile.loadFromFile(context);
}
if (bm == null) {
// 从网络获取数据
bm = loadFromNet(context);
// 存入本地文件
imageFile.Insert2File(context,bm);
}
return bm;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
MainActivity.img.setImageBitmap(bitmap);
}
};
asyncTask.execute(useCache);
}
// 从网络获取数据
private Bitmap loadFromNet(Context context) {
String urlImg = "http://www.imooc.com/api/teacher?type=1";
Bitmap bm = null;
// 发请求获取json数据
String jsonstr = HttpUtils.doGet(urlImg).toString();
// 当jsonstr不为空时,解析json数据
if (jsonstr != null) {
bm = parseJsonstr(jsonstr);
}
return bm;
}
// 解析数据获取图片链接,并下载图片
private Bitmap parseJsonstr(String jsonstr) {
JSONObject root;
try {
root = new JSONObject(jsonstr);
String msgstr = root.optString("msg");
if (msgstr.equals("成功")) {
root = root.optJSONObject("data");
String pic = root.optString("pic1");
// 获取图片数据,并返回Bitmap对象
byte[] b = HttpUtils.doGet(pic).toByteArray();
if (b.length != 0) {
return BitmapFactory.decodeByteArray(b, 0, b.length);
}
}
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}
}
MainActivity
除了对控件进行声明以外,通过前面的类的调用,实现图片和书本内容的下载、存储,并设置相应的点击事件
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
public static ImageView img;
private List<Book> mbookList = new ArrayList<>();
private TextView myCollection,title,author,content;
private Button last,collection,next;
private BookBiz bookBiz = new BookBiz();
private ImageBiz imgBiz = new ImageBiz();
private int index = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
loadImageDatas(true);
loadBookDatas(true);
}
// 获取图片数据
private void loadImageDatas(boolean useCache) {
imgBiz.loadImageDatas(this,useCache);
}
// 获取书本数据
private void loadBookDatas(boolean useCache) {
bookBiz.loadBookDatas(this, new BookBiz.CallBack() {
@Override
public void onSuccess(List<Book> bookList) {
Book book = bookList.get(0);
title.setText(book.getTitle());
author.setText(book.getAuthor());
content.setText(book.getContent());
mbookList = bookList;
}
@Override
public void onFailed(Exception e) {
e.printStackTrace();
Toast.makeText(MainActivity.this,"网络加载失败",Toast.LENGTH_SHORT).show();
}
},useCache);
}
// 初始化控件
private void initView() {
myCollection = findViewById(R.id.mycollection_txt);
title = findViewById(R.id.title_index);
author = findViewById(R.id.author_index);
content = findViewById(R.id.content_index);
img = findViewById(R.id.img_index);
last = findViewById(R.id.last_btn);
collection = findViewById(R.id.collection_btn);
next = findViewById(R.id.next_btn);
}
// 主页按钮单击事件
public void indexClick(View v){
Book book;
switch (v.getId()){
case R.id.mycollection_txt:
startActivity(new Intent(this,MyCollectionActivity.class));
break;
case R.id.last_btn:
index--;
if(index < 0){
index += 30;
}
book = mbookList.get(index % 30);
title.setText(book.getTitle());
author.setText(book.getAuthor());
content.setText(book.getContent());
break;
case R.id.collection_btn:
// 先判断MyCollectionActivity里的bookList里是否包含该对象,如果没有则添加,反之提示相关信息
if (!MyCollectionActivity.bookList.contains(mbookList.get(index))) {
MyCollectionActivity.bookList.add(mbookList.get(index));
Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(this, "文章已存在", Toast.LENGTH_SHORT).show();
}
break;
case R.id.next_btn:
index++;
book = mbookList.get(index % 30);
title.setText(book.getTitle());
author.setText(book.getAuthor());
content.setText(book.getContent());
break;
}
}
}
MyCollectionActivity
在这个类中只需要实现ListView控件的布局适配器以及对ListView子项的点击事件即可
public class MyCollectionActivity extends AppCompatActivity {
public static List<Book> bookList = new ArrayList<>();
private ListView books;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_collection);
init();
initEvent();
}
private void initEvent() {
// ListView子项的点击事件
books.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
ReadActivity.book = bookList.get(position);
startActivity(new Intent(MyCollectionActivity.this,ReadActivity.class));
}
});
}
private void init() {
books = findViewById(R.id.books_listview);
String[] data = new String[bookList.size()];
// 设置数组适配器
for (int i = 0; i < bookList.size(); i++) {
data[i] = bookList.get(i).getTitle();
Log.e(MainActivity.TAG, "initView: "+bookList.get(i).getTitle());
}
ArrayAdapter adapter = new ArrayAdapter(this, R.layout.item_books_listview, data);
books.setAdapter(adapter);
}
}
ReadActivity
此类完成对我的收藏界面中选中书籍信息的显示
public class ReadActivity extends AppCompatActivity {
public static Book book = null;
private TextView title,author,content;
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_read);
initView();
}
private void initView() {
title = findViewById(R.id.title_read);
author = findViewById(R.id.author_read);
content = findViewById(R.id.content_read);
title.setText(book.getTitle());
author.setText(book.getAuthor());
content.setText(book.getContent());
}
}
这次编码参照慕课网的教学视频,对不同业务不同功能板块的代码进行了封装,降低了耦合度,这样做的好处一是可以使代码更加美观整洁、整个业务逻辑更加清晰,二是利于多人协作并行编码