基于rxjava实现的小说练习项目
界面展示
目录界面
章节界面
使用框架
room访问数据库
navigation界面导航
okhttp网络请求
viewmodel管理数据
jsoup网络爬虫
rxjava线程优雅
使用技术
stream优雅处理流
代码展示
github: https://github.com/15029291643/novel2
项目详解
添加依赖
// room
def room_version = "2.4.2"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// optional - RxJava2 support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// rxjava
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.9'
// retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.2'
// jsoup
implementation 'org.jsoup:jsoup:1.13.1'
// Navigation
implementation "androidx.navigation:navigation-fragment:2.3.5"
implementation "androidx.navigation:navigation-ui:2.3.5"
根据逻辑划分包
实现logic逻辑层
判断可行性
爬取链接
目录:蛇王诅咒:妈咪要下蛋
章节:解剖双头蛇王
链接数据
目录界面F12进入
可以从页面源代码中直接获取到对应章节1的标题3和链接2
章节界面F12进入
可以很轻松的在源代码中找到章节13对应的标题2和内容4
显而易见,数据可以轻松得到,只要我们拥有一个Jsoup爬虫框架
在这之前先生成两个数据类
实现Model层
Chapter
import java.util.List;
public class Chapter {
private String title;
private String url;
private List<String> content;
public Chapter() {
}
public Chapter(String title, String url) {
this.title = title;
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public List<String> getContent() {
return content;
}
public void setContent(List<String> content) {
this.content = content;
}
@Override
public String toString() {
return "Chapter{" +
"title='" + title + '\'' +
", url='" + url + '\'' +
", content=" + content +
'}';
}
}
Novel
import java.util.List;
public class Novel {
private String title;
private String url;
private List<Chapter> catalog;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public List<Chapter> getCatalog() {
return catalog;
}
public void setCatalog(List<Chapter> catalog) {
this.catalog = catalog;
}
@Override
public String toString() {
return "Novel{" +
"title='" + title + '\'' +
", url='" + url + '\'' +
", catalog=" + catalog +
'}';
}
}
实现网络请求层
ConstantUtils
public interface ConstantUtils {
String NOVEL_URL = "https://www.258xswz.com/l/6791.html";
String CHAPTER_URL = "https://www.258xswz.com/y/6791-2976071.html";
String BASE_URL = "https://www.258xswz.com/";
}
import com.example.rxjava.logic.model.Chapter;
import com.example.rxjava.logic.model.Novel;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/*
* Jsoup工具类
* 处理html数据
* */
public class JsoupUtils {
private static final String TAG = "JsoupUtils";
// 从html中获取Novel对象
public static Novel getNovel(String html) {
// 使用xpath解析数据
Document parse = Jsoup.parse(html);
String title = parse.selectFirst("body > div.chapterlist > h2 > strong").text();
Elements elements = parse.select("#myarticle > table > tbody > tr > td > a");
// 使用流完成对象转换
List<Chapter> catalog = elements.stream().map(element ->
new Chapter(element.text(),
ConstantUtils.BASE_URL + element.attr("href")))
.collect(Collectors.toList());
Novel novel = new Novel();
novel.setTitle(title);
novel.setCatalog(catalog);
return novel;
}
// 从html中获取Novel对象
public static Chapter getChapter(String html) {
// 使用xpath解析数据
Document parse = Jsoup.parse(html);
String title = parse.selectFirst("#title").text();
String content = parse.select("body > div.chapterlist > div.Readarea").text();
// 解析后是一个字符串,用split进行分割
ArrayList<String> content2 = new ArrayList<>(Arrays.asList(content.split(" ")));
Chapter chapter = new Chapter();
chapter.setTitle(title);
chapter.setContent(content2);
return chapter;
}
}
Okhttp实现网络请求
ClientUtils
import okhttp3.OkHttpClient;
/*
* 网络请求工具类
* 提供Client单例
* */
public class ClientUtils {
private static volatile OkHttpClient sInstance;
public static OkHttpClient getInstance() {
if (sInstance == null) {
synchronized (ClientUtils.class) {
if (sInstance == null) {
sInstance = new OkHttpClient();
}
}
}
return sInstance;
}
}
OkhttpUtils
import com.example.rxjava.logic.model.Chapter;
import com.example.rxjava.logic.model.Novel;
import java.io.IOException;
import okhttp3.Request;
import okhttp3.Response;
/*
* 网络请求工具类
* 访问url链接
* 向JsoupUtils提供数据
* */
public class OkhttpUtils {
private static String getHtml(String url) {
Request request = new Request.Builder().url(url).build();
try {
Response response = ClientUtils.getInstance().newCall(request).execute();
return new String(response.body().bytes(), "gbk");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static Novel getNovel(String url) {
Novel novel = JsoupUtils.getNovel(getHtml(url));
novel.setUrl(url);
return novel;
}
public static Chapter getChapter(String url) {
Chapter chapter = JsoupUtils.getChapter(getHtml(url));
chapter.setUrl(url);
return chapter;
}
}
RxjavaUtils
import android.annotation.SuppressLint;
import com.example.rxjava.logic.model.Chapter;
import com.example.rxjava.logic.model.Novel;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
/*
* Rxjava工具类
* 提供Flowable对象
* */
public class RxjavaUtils {
@SuppressLint("CheckResult")
public static Flowable<Novel> getNovel(String url) {
return Flowable.create(emitter ->
emitter.onNext(OkhttpUtils.getNovel(url)), BackpressureStrategy.ERROR);
}
public static Flowable<Chapter> getChapter(String url) {
return Flowable.create(emitter ->
emitter.onNext(OkhttpUtils.getChapter(url)), BackpressureStrategy.ERROR);
}
}
MainViewModel
import android.annotation.SuppressLint;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import com.example.rxjava.logic.model.Chapter;
import com.example.rxjava.logic.model.Novel;
import com.example.rxjava.logic.network.OkhttpUtils;
import com.example.rxjava.logic.network.RxjavaUtils;
import java.util.function.Function;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
public class MainViewModel extends ViewModel {
private final CompositeDisposable mDisposable = new CompositeDisposable();
private final MutableLiveData<Novel> mNovel = new MutableLiveData<>();
private final MutableLiveData<Chapter> mChapter = new MutableLiveData<>();
private final MutableLiveData<Integer> mPosition = new MutableLiveData<>(0);
private final LiveData<String> mTitle = Transformations.map(mChapter, chapter ->
"第" + (mPosition.getValue() + 1) + "章 " + chapter.getTitle());
private final LiveData<String> mContent = Transformations.map(mChapter, input ->
input.getContent()
.stream()
.reduce(" ", (s1, s2) -> s1 + "\n\n" + " " + s2));
public MutableLiveData<Novel> getNovel() {
return mNovel;
}
public LiveData<String> getTitle() {
return mTitle;
}
public void setPosition(int position) {
mPosition.setValue(position);
setChapter(mNovel.getValue().getCatalog().get(position).getUrl());
}
public void toNext() {
if (mPosition.getValue() != mNovel.getValue().getCatalog().size() - 1) {
setPosition(mPosition.getValue() + 1);
}
}
public LiveData<String> getContent() {
return mContent;
}
public void setNovel(String url) {
mDisposable.add(RxjavaUtils.getNovel(url)
.subscribeOn(Schedulers.io())
.subscribe(mNovel::postValue));
}
private void setChapter(String url) {
mDisposable.add(RxjavaUtils.getChapter(url)
.subscribeOn(Schedulers.io())
.subscribe(mChapter::postValue));
}
}
基于此,所有数据层面已经全部完成,但其中应用框架部分还没讲,等先把那部分更新完后先继续讲