GoGo's home


  • 首页

  • 归档

jvm学习

发表于 2017-10-30

android组件化学习

发表于 2017-10-04

网上关于组件化的文章有很多了,大部分都知道实现的思路,不过还是要自己去实践一下才会发现问题。文章的项目地址,项目用到CleanArchitecture框架,本文会介绍CleanArchitecture框架和dagger2在组件化的使用。

项目关系图

关系图.png

sdk: 一些公用库,各种辅助类,和第三方view
basic: (依赖sdk) 网络访问初始化,本地缓存和第三方包等。
commonbusiness: (依赖basic) 这里为什么我会多出这一层,因为有很多公共的业务,好比公司的app是强登录的,我会把登录模块写在这里,里面也包含了一些baseActivity和BaseApplication和各个组件的一些公共方法还有组件各种的服务接口的定义。
module_archives和module_knowledge: 就是两个组件,可以单独运行。

application和library切换

想必大家都知道了,定义一个isBuildModule=false,在组件的build.gradle中加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (isBuildModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
apply from: 'maven-release-kline-aar.gradle'
}
sourceSets {
main {
if (isBuildModule.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
java {
exclude '**/debug/**'
}
}
}
}

maven-release-kline-aar.gradle是一个打包aar的文件,在切换的时候也会使用不同的AndroidManifest.xml,因为在组件是debug的时候它是单独单独运行的,还有就是代码可以在建立一个debug包,可以在单独运行的时候做些初始化app的,打包的时候回剔除这部分代码。

library依赖和资源问题

我把所以的library都依赖在basic,每个组件都会依赖这个包,这样就不会存在library的版本问题,资源的问题就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
defaultConfig {
if (isBuildModule.toBoolean()) {
applicationId "com.wkw.archives"
}
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
resourcePrefix "archives"
}

组件之间的通信

你可以选择阿里的ARouter库,但是我的项目没有那么复杂,组件也就4个左右,所以没有使用阿里的库,我采用的是以下就是核心代码

1
2
3
4
5
6
Object result = null;
Class<?> c = Class.forName(className);
if (c != null) {
result = c.newInstance();
}
return result;

但是采用Class.forName会有个问题那就是在混淆的时候,className是指定的,所以要在类上加@Keep 。

CleanArchitecture框架和dagger在组件化的使用

CleanArchitecture框架的github地址,这里再介绍分享一篇文章小鄧子的Easy Clean architecture on Android,我把data和domain会写在各自的业务模块中,自己的模块只要定义自己的就可以了,有个ApplicationModule会定义一些每个模块都需要的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ApplicationModule {
@Provides
@Singleton
Context provideContext(Application application) {
return application;
}
@Provides
@Singleton
UserSystem provideUserSystem() {
return new UserSystem();
}
@Provides
@Singleton
ThreadExecutor provideThreadExecutor(JobExecutor jobExecutor) {
return jobExecutor;
}
@Provides
@Singleton
PostExecutionThread providePostExecutionThread(UIThread uiThread) {
return uiThread;
}
@Provides
@Singleton
MrService provideMrService() {
return new MrService();
}
@Provides
@Singleton
UserCache provideUserCache(UserCacheImpl userCache) {
return userCache;
}
}

然后在module_archives模块中会有ArchivesDataRepositoryModule和ArchivesActivityModule
其中KnowledgeDataRepositoryModule用于提供如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Module
public class ArchivesDataRepositoryModule {
@Provides
@Singleton
ArchivesApi providesArchivesApi(MrService mrService) {
return mrService.createApi(ArchivesApi.class);
}
@Provides
@Singleton
ArchivesRepository prvidesArchivesRepository(ArchivesDataRepository archivesDataRepository) {
return archivesDataRepository;
}
}

ArchivesActivityModule的代码如下:

1
2
3
4
5
6
@Module
public abstract class ArchivesActivityModule {
@PerActivity
@ContributesAndroidInjector()
abstract ArchivesActivity contributeArchivesActivity();
}

这样在主app的AppComponent类中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Singleton
@Component(modules = {
AndroidInjectionModule.class, ApplicationModule.class,
ArchivesDataRepositoryModule.class, ArchivesActivityModule.class,
KnowledgeDataRepositoryModule.class, KnowledgeActivityModule.class
})
public interface AppComponent {
@Component.Builder
interface Builder {
@BindsInstance
Builder application(Application application);
AppComponent build();
}
void inject(MrApplication mrApplication);
}

在module_archives为debug模式下也会有个AppComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Singleton
@Component(modules = {
AndroidInjectionModule.class, ApplicationModule.class,
ArchivesDataRepositoryModule.class, ArchivesActivityModule.class
})
public interface AppComponent {
@Component.Builder
interface Builder {
@BindsInstance
Builder application(Application application);
AppComponent build();
}
void inject(ArchivesApplication mrApplication);
}

各种模块初始化api和Repository,你要是嫌弃每个模块都要引入两个.class文件,你可以使用一个然后采用include的方式好比dagger中的AndroidSupportInjectionModule类方式

1
2
3
4
5
6
7
8
9
@Beta
@Module(includes = AndroidInjectionModule.class)
public abstract class AndroidSupportInjectionModule {
@Multibinds
abstract Map<Class<? extends Fragment>, AndroidInjector.Factory<? extends Fragment>>
supportFragmentInjectorFactories();
private AndroidSupportInjectionModule() {}
}

这样就会很清楚的知道自己模块需要初始化什么和使用什么,也不用考虑其它模块的初始化的数据,之后只需要在主app加入就行,也是比较方便的。

打包

各种模块当为library是要打包成aar的,maven-release-kline-aar.gradle文件代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
apply plugin: 'maven'
ext {// ext is a gradle closure allowing the declaration of global properties
PUBLISH_GROUP_ID = 'com.wkw'
PUBLISH_ARTIFACT_ID = 'archives'
PUBLISH_VERSION = rootProject.ext.versionName
}
uploadArchives {
repositories.mavenDeployer {
//这里就是最后输出地址,在自己电脑上新建个文件夹,把文件夹路径粘贴在此
//注意”file://“ + 路径,有三个斜杠,别漏了
repository(url: "file:///Users/wukewei/Documents/android/ModularizationExample/repo")
pom.project {
groupId project.PUBLISH_GROUP_ID
artifactId project.PUBLISH_ARTIFACT_ID
version project.PUBLISH_VERSION
}
}
}
//以下代码会生成jar包源文件,如果是不开源码,请不要输入这段
//aar包内包含注释
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.sourceFiles
}
artifacts {
archives androidSourcesJar
}

我只是打包到本地,你可以自己建立一个本地maven库。

如要改进的地方

1:每次建立一个模块还是要做很多初始化的工作
2:maven-release-kline-aar.gradle每个模块都会有,希望后期能改进。
3: debug模式下各个模块登录之后跳转问题
4:等等。。。。。

参考

从零开始的Android新项目11 - 组件化实践(1) -
大帅

Kotlin学习之hot_kotlin

发表于 2017-10-04

自从google IO大会之后,kotlin的热度一直在上升,kotlin官方文档,这里也有个中文的中文文档,作为android开发也可以看Kotlin for Android Developers ,接下来就说说在android方面的使用,记录一下自己的学习,大部分是通过Kotlin for Android Developers 学习到的。

Delegated Properties的使用(委托属性)

(1)这里简单的说明一下:用var修饰的可变属性和由val修饰的只读属性。由val修饰的只读属性使用的委托需要实现ReadOnlyProperty,而var修饰的可变属性则需要实现ReadWriteProperty。在调用被委托的属性的getter和setter时,对应操作会被委托给getValue()以及setValue()。
因为我在项目中使用了dagger2所以在app上要初始化ApplicationComponent提供给其它的component作为依赖,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class HotApp : Application() {
companion object {
var graph: ApplicationComponent by DelegatesExt.notNullSingleValue()
}
override fun onCreate() {
super.onCreate()
graph = DaggerApplicationComponent.builder()
.applicationModule(ApplicationModule(this))
.build()
}
}
object DelegatesExt {
fun <T> notNullSingleValue() = NotNullSingleValueVar<T>()
}
class NotNullSingleValueVar<T> {
private var value: T? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: throw IllegalStateException("${property.name} not initialized")
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = if (this.value == null) value
else throw IllegalStateException("${property.name} already initialized")
}
}

graph属性只会被初始化一次,当你还没有初始化就去使用和多次初始化会报IllegalStateException异常。其中最主要的是getValue和setValue方法。
thisRef —— 必须与 属性所有者 类型(对于扩展属性——指被扩展的类型)相同或者是它的超类型,
property —— 必须是类型 KProperty<*> 或其超类型,
这个函数必须返回与属性相同的类型(或其子类型),
new value —— 必须和属性同类型或者是它的超类型。

(2)Lazy(延迟属性)

大部分app都会使用到toolbar,这时候你可以使用个接口对其统一的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface ToolbarManager {
val toolbar: Toolbar
var toolbarTitle: String
get() = toolbar.title.toString()
set(value) {
toolbar.title = value
}
fun enableHomeAsUp(up: () -> Unit) {
toolbar.navigationIcon = createUpDrawable()
toolbar.setNavigationOnClickListener { up() }
}
private fun createUpDrawable() = DrawerArrowDrawable(toolbar.ctx).apply { progress = 1f }
}

使用的时候

1
2
3
4
5
6
7
class MainActivity : AppCompatActivity(), ToolbarManager {
override val toolbar by lazy {
tool_bar
}
...省略代码...
}

因为当你不使用lazy关键字的时候,因为view都还没有创建所以一定找不到的。

(3)Observable(可观察属性)
Delegates.observable()

接受两个参数:初始值和修改时处理程序(handler), 每当我们给属性赋值时会调用该处理程序(在赋值后执行),它有三个参数:被赋值的属性、旧值和新值。在android使用就是adapter的使用

1
var populars: List<PopularModel> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }

就是在adapter的populars属性变化的时候调用notifyDataSetChanged() 方法,后期为了为了学习DiffUtil加上了,把之前的代码去掉了,因为我的viewmodel是data类,会自动推断产生以下函数,equals()/hashCode() pair,toString(),componentN(),copy()这样就会方便使用。

Extensions(扩展函数)

用户经常会短时间多次点击按钮,所以我们可以对View进行扩展,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun View.singleClick(l: (android.view.View?) -> Unit) {
setOnClickListener(SingleClickListener(
}
class SingleClickListener(val click: (v: View) -> Unit) : View.OnClickListener {
companion object {
private val DOUBLE_CLICK_TIMEOUT = ViewConfiguration.getDoubleTapTimeout()
}
private var lastClick: Long = 0
override fun onClick(v: View) {
if (getLastClickTimeout() > DOUBLE_CLICK_TIMEOUT) {
lastClick = System.currentTimeMillis()
click(v)
}
}
private fun getLastClickTimeout(): Long {
return System.currentTimeMillis() - lastClick
}
}

这样View就有singleClick()函数了,其实你对代码进行Bytecode会发现

1
2
3
4
5
6
7
public final class ViewExtensionsKt {
public static final void singleClick(@NotNull View $receiver, @NotNull Function1 l) {
Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
Intrinsics.checkParameterIsNotNull(l, "l");
$receiver.setOnClickListener((OnClickListener)(new SingleClickListener(l)));
}
}

这不就是java的静态方法,这样你就可以根据自己的项目要求去愉快的扩展了,Extensions are resolved statically,扩展函数是静态解析的。

inline function(内联函数)

以下代码来自anko,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
inline fun <reified T : Activity> Context.startActivity(vararg params: Pair<String, Any>) {
Internals.internalStartActivity(this, T::class.java, params)
}
object Internals {
@JvmStatic
fun internalStartActivity(
ctx: Context,
activity: Class<out Activity>,
params: Array<out Pair<String, Any>>
) {
ctx.startActivity(createIntent(ctx, activity, params))
}
@JvmStatic
fun <T> createIntent(ctx: Context, clazz: Class<out T>, params: Array<out Pair<String, Any?>>): Intent {
val intent = Intent(ctx, clazz)
if (params.isNotEmpty()) fillIntentArguments(intent, params)
return intent
}
@JvmStatic
private fun fillIntentArguments(intent: Intent, params: Array<out Pair<String, Any?>>) {
params.forEach {
val value = it.second
when (value) {
null -> intent.putExtra(it.first, null as Serializable?)
is Int -> intent.putExtra(it.first, value)
is Long -> intent.putExtra(it.first, value)
is CharSequence -> intent.putExtra(it.first, value)
is String -> intent.putExtra(it.first, value)
is Float -> intent.putExtra(it.first, value)
is Double -> intent.putExtra(it.first, value)
is Char -> intent.putExtra(it.first, value)
is Short -> intent.putExtra(it.first, value)
is Boolean -> intent.putExtra(it.first, value)
is Serializable -> intent.putExtra(it.first, value)
is Bundle -> intent.putExtra(it.first, value)
is Parcelable -> intent.putExtra(it.first, value)
is Array<*> -> when {
value.isArrayOf<CharSequence>() -> intent.putExtra(it.first, value)
value.isArrayOf<String>() -> intent.putExtra(it.first, value)
value.isArrayOf<Parcelable>() -> intent.putExtra(it.first, value)
else -> throw RuntimeException("Intent extra ${it.first} has wrong type ${value.javaClass.name}")
}
is IntArray -> intent.putExtra(it.first, value)
is LongArray -> intent.putExtra(it.first, value)
is FloatArray -> intent.putExtra(it.first, value)
is DoubleArray -> intent.putExtra(it.first, value)
is CharArray -> intent.putExtra(it.first, value)
is ShortArray -> intent.putExtra(it.first, value)
is BooleanArray -> intent.putExtra(it.first, value)
else -> throw RuntimeException("Intent extra ${it.first} has wrong type ${value.javaClass.name}")
}
return@forEach
}
}
}

这段代码包含的知识点很多,这里说一下reified修饰符“具体化”, 几乎就像是一个普通的类一样,正常的操作符如 !is 和 as 现在都能用了,在调用的时候要是具体的类型而不能是范型。

项目介绍:hot项目之前使用java的时候写过链接,这里就不再过多的介绍了,kotlin_hot地址,项目采用第三方框架如下:

  • Rxjava
  • MVP
  • Dagger2
  • Retrofit
  • OkHttp3
  • Glide
  • Kotlin
    通过此次的学习自己也加深了对kotlin的理解,虽然还处于小白阶段,希望能帮助到你,要是有写的不对的地方望指出,一起学习进步。

databinding和CleanArchitecture框架的使用

发表于 2017-10-04

最近在做一个公司内部的app,框架方面使用了CleanArchitecture,因为本身app的业务逻辑不是很复杂,所以在使用的时候我没有写presenter这一层,只有当页面逻辑比较复杂的时候还会去写presenter层,主要还是懒得去写那么多接口。

databinding的使用

textview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<TextView
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:text="@{item.startedStatus}"
android:textColor="@color/dicuss_content_text_color"
tools:text="启动状态"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="@{@string/monthperformance(totalContributionCount, totalInGroupCount)}"
android:textColor="@color/white"
android:textSize="13sp"
tools:text="总计:贡献额:3928191.21 元 入组数:102910 例"/>

使用多个的的时候在strings里面定义的记得要写上%1和%2要不然会报错的。

1
<string name="monthperformance">总计:贡献额:%1$d 元 入组数:%2$d 例</string>

view的VISIBLE和GONE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<data>
<import type="android.view.View"/>
<variable
name="item"
type="com.xxx.xxxx.model.QuestionModel"/>
<variable
name="presenter"
type="com.xxx.xxx.view.business.common.DetailActivity.Presenter"/>
</data>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onTopClick(item)}"
android:src="@{item.topStatus ? @drawable/ic_had_up : @drawable/ic_up}"
android:visibility="@{item.myQuestion ? View.VISIBLE : View.GONE}"
tools:src="@drawable/ic_had_up"/>

这部分很好的说明了点击事件的使用和view的显示和隐藏,也可以根据状态来选择不同的src。

onLongClick事件

1
2
3
4
5
6
7
8
9
10
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:onClick="@{() -> presenter.onItemClick(item)}"
android:onLongClick="@{() -> presenter.onItemLongClick(item)}"
android:orientation="vertical"
android:paddingRight="25dp"
android:visibility="@{item.showProgress ? View.INVISIBLE : View.VISIBLE}">
....省略代码

记得在定义方法的时候记得要有个boolean类型的返回,当时没有注意到导致了databinding生成不了,后来看了官网才发现,怪自己啊。。。

1
2
3
4
public boolean onItemLongClick(FileModel model) {
return true;
}

RecyclerView方面的使用

使用了databindingAdapter,代码非常的简单,使用起来也非常的方便好比

1
2
3
4
5
6
7
mAdapter = new MultiTypeAdapter(this);
mAdapter.addViewTypeToLayoutMap(PROJECT_VIEW_TYPE, R.layout.item_discuss_project);
mAdapter.addViewTypeToLayoutMap(STUDY_VIEW_TYPE, R.layout.item_discuss_study);
mAdapter.addViewTypeToLayoutMap(EXPERIENCE_VIEW_TYPE, R.layout.item_discuss_experience);
mAdapter.setPresenter(new Presenter());
getRecyclerViewWithFooter().setAdapter(mAdapter);
getRecyclerViewWithFooter().setVerticalLinearLayout();

这是我RecyclerView多个布局的定义,在xml中使用

1
2
3
4
5
6
7
8
9
10
11
12
<data>
<import type="android.view.View"/>
<variable
name="item"
type="com.xxx.xxx.model.CommentModel"/>
<variable
name="presenter"
type="com.xxx.xxx.listener.CommentPresenter"/>
</data>

就是数据的name为item和事件的name为presenter。更多的使用可以自己去看github上的介绍。

图片访问方面

因为这个app图片的使用上还是蛮多的,所以选择了Fresco(也看中它能在xml中写一些配置),不太好的地方就是包的体积大了些,但是还是能接受的。

1
2
3
4
5
6
7
<com.xxx.xxx.view.widget.MyDraweeView
android:layout_width="@dimen/user_avater_w"
android:layout_height="@dimen/user_avater_w"
app:failureImage="@drawable/ic_avatar"
app:placeholderImage="@drawable/ic_avatar"
app:roundAsCircle="true"
app:url="@{item.avatar}"/>

这里的MyDraweeView是继承SimpleDraweeView因为fresco是不支持自适应的,所以在这里你可以自己设置view的LayoutParams来达到目的。

CleanArchitecture框架的使用

api和repository

api和repository,我把各个模块的api都分开定义,这样在分模块开发的时候,可以各自定义api
api.png

module设计

dagger,在定义module的时候我采用的是new的方式,而不是使用在构造函数上加@Inject,因为采用new的方式能达到局部单例的效果,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Module
public class ProjectModule {
public ProjectModule() {
}
@Provides
@PerActivity
ProjectSearchUseCase provideProjectSearchUseCase(ProjectRepository projectRepository,
ThreadExecutor threadExecutor,
PostExecutionThread postExecutionThread) {
return new ProjectSearchUseCase(projectRepository, threadExecutor, postExecutionThread);
}
}
1
2
3
4
5
6
7
this.provideProjectHistoryUseCaseeProvider =
DoubleCheck.provider(
ProjectModule_ProvideProjectHistoryUseCaseeFactory.create(
builder.projectModule,
userRepositoryProvider,
threadExecutorProvider,
postExecutionThreadProvider));

这里的doublecheck就是起到这里这个效果,dagger中的单例也是如此,而不是你加上@Singleton就是单例了,是要在Application中才能达到单例的效果。这里就不展开了。

component设计

component也是采用的分模块定义,在fragment和activity使用上我也是采用和github上一样的方法在activity上提供component,activity实现HasComponent接口,在fragment里面

1
2
3
protected <C> C getComponent(Class<C> componentType) {
return componentType.cast(((HasComponent<C>) getActivity()).getComponent());
}

就能获取到activity的component了。

数据模型的转换

CleanArchitecture中的各种DataMapper,之前不是很理解这个是干什么用的,github上在data层和presentation两层同时使用了。后来在实践的时候发现presentation这层的转化还是非常有必要的。当后台接口返回的数据格式不是你想要的时候,(后台还懒得去帮你改)你可以定义一层转换,还是就是viewmodel中还包含了一些接口返回没有的字段,好比一个view是否显示
datamapper.png当后台返回的数据类型不是你想要的好比boolean类型返回的是string的‘0’和‘1’,还有就是null的判断,尽量的让view获取到的数据不是空对象。

缓存

缓存方面,本身app就没有多大的缓存,只是记录一下搜索记录,没有引入什么第三方的数据库之类的,只是很简单的把搜索记录存入一个文件中

1
2
3
4
5
6
7
8
9
10
if (!TextUtils.isEmpty(content)) {
final File file = this.buildFile(fileName);
List<String> history = getHistoryList(fileName, false);
if (history.contains(content)) {
history.remove(content);
}
history.add(content);
final String jsonString = mGson.toJson(history).toString();
this.executeAsynchronously(new CacheWriter(mFileManager, file, jsonString));
}

在每次搜索的时候保存搜索记录

1
2
3
4
5
6
7
8
9
10
@Override
public Observable<ProjectEntity> getSearchData(ProjectSearch search, boolean isSave) {
return mProjectApi.getListAndSearch(search)
.doOnNext(entity -> {
if (isSave) {
ProjectDataRepository.this.mCacheImp.putProjectSearchHistory(search.getKey_Words());
}
})
.compose(RepositoryUtils.handleResult());
}

RecyclerView嵌套webview的问题

记录一次RecyclerView嵌套webview的问题,一开始我直接在layout布局里面是这样写的,发现页面显示不全(原因未找到。。。要是有人知道原因欢迎告知)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<WebView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:weburl="@{item.studyContent}"/>
<include layout="@layout/layout_detail_discuss_header"/>
</LinearLayout>

解决方法就是自定义一个view继承LinearLayout,采用动态生成webview这样就能显示全了。

1
2
3
4
5
6
7
8
9
10
11
12
setOrientation(VERTICAL);
mWeb = new WebView(getContext().getApplicationContext(), null);
WebSettings setting = mWeb.getSettings();
setting.setJavaScriptEnabled(true);
setting.setBuiltInZoomControls(true);
setting.setUseWideViewPort(true);
setting.setLoadWithOverviewMode(true);
setting.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
addView(mWeb, params);
LinearLayout tagViewsLayout = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.layout_detail_discuss_header, null);
addView(tagViewsLayout);

总结

使用databinding的时候确实让我感受到了比平时写的要快一些,再也不是写id了也不用设置值了,写起来还是比较爽的,期间也遇到了一些问题,就是使用databinding和dagger一起使用的时候报错,有时候定位不是很准确,但是只要认真看看还是能找得到错误之处的,可能是因为我还是能力还不够的问题。通过这次的实践,让我更加深刻的理解了CleanArchitecture这个框架,也体会到了一些好的框架带来的好处。

FragmentTabHost使用遇到的问题

发表于 2017-10-04

本文记录一下在使用fragmentTabHost的时候所遇到的问题。

fragment的重新建立的原因

原生的fragmenttabhost在切换tab的时候,会导致fragment的重新建立

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private FragmentTransaction doTabChanged(String tabId, FragmentTransaction ft) {
.......省略部分代码
if (mLastTab != newTab) {
if (ft == null) {
ft = mFragmentManager.beginTransaction();
}
if (mLastTab != null) {
if (mLastTab.fragment != null) {
ft.detach(mLastTab.fragment);
}
}
if (newTab != null) {
if (newTab.fragment == null) {
newTab.fragment = Fragment.instantiate(mContext,
newTab.clss.getName(), newTab.args);
ft.add(mContainerId, newTab.fragment, newTab.tag);
} else {
ft.attach(newTab.fragment);
}
}
mLastTab = newTab;
}
return ft;
}

很清楚的看到在切换的时候上个fragment调用detach,新的fragment调用的是attach,所以会导致重新初始化。

解决办法

就是把源码复杂一份,修改这个部分代码就可以解决了。

1
2
3
4
5
6
7
8
9
10
11
12
if (mLastTab != null) {
if (mLastTab.fragment != null) {
ft.hide(mLastTab.fragment); //detach改为hide
}
}
if (newTab.fragment == null) {
newTab.fragment = Fragment.instantiate(mContext,
newTab.clss.getName(), newTab.args);
ft.add(mContainerId, newTab.fragment, newTab.tag);
} else {
ft.show(newTab.fragment); //attach改为show
}

要求是当点击其中一个tab的时候不导致fragment的切换,而是做一些其他的操作

解决办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class NoTabFragmentTabHost extends FragmentTabHost {
private String mCurrentTag;
private String mNoTabChangedTag;
public NoTabFragmentTabHost(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onTabChanged(String tag) {
if (tag.equals(mNoTabChangedTag)) {
setCurrentTabByTag(mCurrentTag);
} else {
super.onTabChanged(tag);
mCurrentTag = tag;
}
}
public void setNoTabChangedTag(String tag) {
this.mNoTabChangedTag = tag;
}
}

app当再次点击当前tab的时候会去刷新操作

1
mTabHost.getTabWidget().getChildAt(i).setOnTouchListener(this);

给每个的tabwidget的子view设置监听事件,再onTouch的处理

1
2
3
4
5
6
7
8
9
boolean consumed = false;
if (event.getAction() == MotionEvent.ACTION_DOWN
&& v.equals(mTabHost.getCurrentTabView())) {
Log.d("再次点击了", "再次点击了");
consumed = true;
}
}
return consumed;

在这里你就可以获取当前的fragment再处理逻辑了。

原生项目(hot)集成react-native

发表于 2017-10-04

最近react-native(以下简称rn)的技术越来越活,国内的使用情况也越来越多,国内的氛围也越来越好,其实我是从去年就接触了一下rn,当时我学习了一个多星期,然后刚好处于要换工作就没有学习了,刚好最近有点空闲的时间,就再次去学习,只能说rn的更新速度太快了,很多之前的写法都变了,只能从头开始,但是一整个项目都采用rn来构建还不是很现实,所以国内很多的也是集成rn,所以我也来凑凑热闹。

添加依赖

在项目的build.gradle下添加

1
2
3
4
5
6
7
8
9
10
allprojects {
repositories {
mavenLocal()
jcenter()
maven {
// All of React Native (JS, Android binaries) is installed from npm
url "$rootDir/node_modules/react-native/android"
}
}
}

app的实现ReactApplication接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
*/
public class App extends Application implements ReactApplication{
private static App appContext;
private static AppComponent mAppComponent;
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
};

AndroidManifest.xml

1
2
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>

加入以下的activity可以在摇一摇出现rn的配置页面。

activity来继承ReactActivity

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "MyHot";
}
}

在这里介绍一篇大帅写的React Native 0.31 Bundle 预加载优化主要解决从原生页面到rn页面会有短暂的白屏。使用方法

1
2
3
dependencies {
compile "com.github.moduth:react-native-preloader:0.31.0"
}

把之前继承ReactActivity变成继承MrReactActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyReactActivity extends MrReactActivity {
public static final ReactInfo reactInfo = new ReactInfo("MyHot", null);
@Override
protected String getMainComponentName() {
return reactInfo.getMainComponentName();
}
@Override
public ReactInfo getReactInfo() {
return reactInfo;
}
}

在调用rn页面调用

1
ReactPreLoader.init(this, MyReactActivity.reactInfo);

这样就能很好的解决上述的问题。

react-native端的代码

在rn中我引入了redux,以下就贴出关键行代码,在最外层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import configureStore from './store/store.js'
import App from './containes/App.js'
const store = configureStore();
class rootApp extends Component {
render() {
return (
<Provider store={store}>
<App/>
</Provider>
)
}
}
export default rootApp;

同时个actions的文件夹里面放着各种的操作,以下是我的newsList的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import * as types from "../constants/ActionTypes.js";
import * as Services from "../utils/Services.js";
import {
NEW_LIST
} from "../constants/Urls.js";
export function fetchNews(loading, isRefresh, isLoadMore, pn) {
return dispatch => {
dispatch(fetchNewsList(loading, isRefresh, isLoadMore));
Services.getNewList(NEW_LIST, pn)
.then(result => {
dispatch(receiveNewsList(result));
});
}
}
function fetchNewsList(loading, isRefresh, isLoadMore) {
return {
type: types.FE_NEWLIST,
loading: loading,
isRefresh: isRefresh,
isLoadMore: isLoadMore
};
}
function receiveNewsList(newsList) {
return {
type: types.RE_NEWLIST,
newsList: newsList
}
}

还有就是ruducers文件夹下的和action所对应的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import * as types from '../constants/ActionTypes';
const initialState = {
loading: true,
isRefresh: false,
isLoadMore: false,
newsList: []
}
export default function revicesNewsList(state = initialState, action) {
switch (action.type) {
case types.FE_NEWLIST:
return Object.assign({}, state, {
loading: action.loading,
isRefresh: action.isRefresh,
isLoadMore: action.isLoadMore,
});
case types.RE_NEWLIST:
return Object.assign({}, state, {
loading: false,
isRefresh: false,
isLoadMore: false,
newsList: state.isLoadMore ? state.newsList.concat(action.newsList) : action.newsList,
});
default:
return state;
}
}

还有就是一个转化的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import {
connect
} from 'react-redux';
import News from "../pages/News.js";
import {
bindActionCreators
} from 'redux';
import * as newsAction from '../actions/newsList.js';
class NewsContainer extends Component {
render() {
return (
<News {...this.props} />
);
}
}
function mapStateToProps(state) {
const {
news
} = state;
return {
news
}
}
function mapDispatchToProp(dispatch) {
return {
actions: bindActionCreators(newsAction, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProp)(NewsContainer);

这样在使用的时候就是调用action的方法

1
2
3
4
5
6
componentDidMount() {
const {
actions
} = this.props;
actions.fetchNews(true, false, false, page);
}

接受到数据

1
2
3
4
5
6
7
8
9
10
11
12
13
render() {
const {
navigator,
news
} = this.props;
return (
<View style={styles.container}>
<Header title="体育" navigator= {navigator}/>
{this.renderContent()}
</View>
)
}

这样以后要是添加新的页面就很清楚了,在action里面添加动作,在reducers添加action的处理器,这样就很清晰了。
我添加了一个Services,来获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const HOST = "http://apis.baidu.com/";
import FetchHttpClient, {
query,
header
} from 'fetch-http-client';
import {
toastShort
} from "./ToastUtil.js";
let client = new FetchHttpClient(HOST);
export function getNewList(url, pnSize) {
return getClient().get(url, {
query: {
num: 10,
pn: pnSize
},
}).then(filterJSON)
.then(filterStatus)
.catch((error) => {
toastShort('网络发生错误,请重试!')
});
}
function getClient() {
client.addMiddleware(query());
client.addMiddleware(request => {
request.options.headers['apikey'] = 'ff27ef67506b2c0738a3252b01f8d564';
});
return client;
}
function filterStatus(res) {
if (res.code === 200) {
return res.newslist;
} else {
toastShort(res.msg);
}
}
function filterJSON(res) {
return res.json();
}

总结

通过这次的学习,让我了解了react-native,因为react-native的周边产品还是很多的,也学习了redux是的reacr-native也有一个小小的框架实现,使得代码结构更加的清晰,本人rn的水平有限,要是有不对的地方,还请指出和谅解。

Hot项目进行部分修改(加入Dagger2)

发表于 2017-10-04

最近在看Dagger2,网上资料也很多,并且在自己私下的项目中Hot运用(一个关于微信热门头条的分享)。这里就不介绍dagger的一些使用方法啊,这里有个很好的介绍dagger介绍

DataManager管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* Created by wukewei on 16/7/12.
* 这个类是管理app的数据来源无论从网络获取.内存.还是磁盘
*/
public class DataManager {
private HotApi mHotApi;
private CacheLoader cacheLoader;
@Inject
public DataManager(HotApi hotApi, CacheLoader cacheLoader) {
this.mHotApi = hotApi;
this.cacheLoader = cacheLoader;
}
/***
* 获取分类的类型
* @param
* @param
* @return
*/
public List<String> getTabs() {
List<String> tabs = new ArrayList<>();
tabs.add("科技");
tabs.add("美女");
tabs.add("生活");
tabs.add("娱乐");
tabs.add("搞笑");
tabs.add("宅男");
return tabs;
}
/***
* 获取列表
* @param pn 页码
* @param type 类别名称
* @return
*/
public Observable<List<Popular>> getPopular(int pn, String type) {
return mHotApi.getPopular(pn, Constants.PAGE_SIZE, type)
.compose(SchedulersCompat.applyIoSchedulers())
.compose(RxResultHelper.handleResult())
.doOnNext(populars -> {
if (pn == 1) {
ListPopular popular = new ListPopular(populars);
cacheLoader.upNewData(type, popular);
}
});
}
/***
* 获取缓存信息 默认只缓存第一页
* @param type 类别名称
* @param
* @return
*/
public Observable<List<Popular>> getCachePopular(String type) {
NetworkCache<ListPopular> networkCache = new NetworkCache<ListPopular>() {
@Override
public Observable<ListPopular> get(String key, Class<ListPopular> cls) {
return mHotApi.getPopular(1, Constants.PAGE_SIZE, type)
.compose(SchedulersCompat.applyIoSchedulers())
.compose(RxResultHelper.handleResult())
.flatMap(populars -> {
ListPopular popular = new ListPopular(populars);
return Observable.just(popular);
});
}
};
return cacheLoader.asDataObservable(Constants.NEW_LIST + type, ListPopular.class, networkCache)
.map(listPopular -> listPopular.data);
}
}

AppModule的构造的对象和提供的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* Created by wukewei on 16/7/19.
*/
@Module
public class AppModule {
private App application;
public AppModule(App application) {
this.application = application;
}
@Provides
@Singleton
@ContextLife("Application")
public App provideApp() {
return application;
}
@Provides
@Singleton
OkHttpClient provideOkHttpClient() {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
Interceptor apikey = chain -> chain.proceed(chain.request().newBuilder()
.addHeader("apikey", Constants.Api_Key).build());
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.readTimeout(Constants.HTTP_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.connectTimeout(Constants.HTTP_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.addInterceptor(apikey)
.addInterceptor(loggingInterceptor)
.build();
return okHttpClient;
}
@Provides
@Singleton
HotApi provideHotApi(OkHttpClient okHttpClient) {
Retrofit retrofit1 = new Retrofit.Builder()
.baseUrl(Constants.Base_Url)
.client(okHttpClient)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build();
HotApi hotApi = retrofit1.create(HotApi.class);
return hotApi;
}
@Provides
@Singleton
CacheLoader provideCacheLoader() {
return CacheLoader.getInstance(application);
}
@Provides
@Singleton
DataManager provideDataManager(HotApi hotApi, CacheLoader cacheLoader) {
return new DataManager(hotApi, cacheLoader);
}
}

接下来的Component的注入的代码

ActivityComponent的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Created by wukewei on 16/7/19.
*/
@PerActivity
@Component(dependencies = AppComponent.class, modules = ActivityModule.class)
public interface ActivityComponent {
DataManager getDataManager();
Activity getActivity();
void inject(MainActivity mainActivity);
void inject(WebActivity webActivity);
}

3.FragmentComponent的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Created by wukewei on 16/7/19.
*/
@PerFragment
@Component(dependencies = AppComponent.class, modules = FragmentModule.class)
public interface FragmentComponent {
DataManager getDataManager();
Activity getActivity();
void inject(ItemFragment itemFragment);
}

之前在View 和P的关联在通过在p构造方法中传入,现在是使用调用p的attachView()方法。

现在BasePresenter设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Created by wukewei on 16/5/26.
*/
public abstract class BasePresenter<T extends IView> implements IPresenter<T> {
protected Activity mActivity;
protected T mView;
protected CompositeSubscription mCompositeSubscription;
protected DataManager dataManager;
public BasePresenter(DataManager dataManager, Activity activity) {
this.dataManager = dataManager;
this.mActivity = activity;
}
@Override
public void attachView(T view) {
this.mView = view;
}
protected void handleError(Throwable throwable) {
ToastUtil.showShort(mActivity, ErrorHanding.handleError(throwable));
}
protected void unSubscribe() {
if (mCompositeSubscription != null) {
mCompositeSubscription.unsubscribe();
}
}
protected void addSubscribe(Subscription subscription) {
if (mCompositeSubscription == null) {
mCompositeSubscription = new CompositeSubscription();
}
mCompositeSubscription.add(subscription);
}
@Override
public void detachView() {
this.mView = null;
unSubscribe();
}
}

BaseActivity中的p之前需要自己new出来,现在加入了dagger2,通过注入的方式

/**

  • Created by wukewei on 16/5/26.
    */
    public abstract class BaseActivity extends AppCompatActivity implements IView {

    @Inject
    protected T mPresenter;
    protected Activity mContext;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);
    setContentView(getLayout());
    ButterKnife.bind(this);
    mContext = this;
    setupActivityComponent(App.getAppComponent(),new ActivityModule(this));
    mPresenter.attachView(this);
    initEventAndData();
    

    }

    protected void setCommonBackToolBack(Toolbar toolbar, String title) {

    toolbar.setTitle(title);
    setSupportActionBar(toolbar);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    getSupportActionBar().setDisplayShowHomeEnabled(true);
    toolbar.setNavigationOnClickListener(v -> onBackPressed());
    

    }

    @Override
    protected void onDestroy() {

    super.onDestroy();
    ButterKnife.unbind(this);
    if (mPresenter != null) mPresenter.detachView();
    

    }

    /**

    • 依赖注入的入口
    • @param appComponent appComponent
      */
      protected abstract void setupActivityComponent(AppComponent appComponent, ActivityModule activityModule);

      protected abstract int getLayout();
      protected abstract void initEventAndData();
      }

      1
      2
      多了setupActivityComponent()方法这就是依赖注入的入口好比。

    @Override
    protected void setupActivityComponent(AppComponent appComponent, ActivityModule activityModule) {

    DaggerActivityComponent.builder()
            .appComponent(appComponent)
            .activityModule(activityModule)
            .build()
            .inject(this);
    

    }
    ````

总结

这个项目是我私下自己在学习新的技术,并且运用在一起的项目,也是像对待产品一个对待这个app开发,后续会不断的学习和更新项目。本人也是个android小菜鸟,从最弱最弱的做起。

Rxjava实现三级缓存的两种方式

发表于 2017-10-04

本文正如标题所说的用rxjava实现数据的三级缓存分别为内存,磁盘,网络,刚好最近在看Android源码设计模式解析与实战(受里面的ImageLoader的设计启发),我把代码放到了我的hot项目中。github地址

使用concat()和first()的操作符

用concat()和first()的操作符来实现,这是我在看Android源码设计模式解析与实战,作者在第一章的时候就介绍ImageLoader的设计。在内存中存储的方式LruCache来实现的,磁盘存储的方式就是序列化存储。

定义一个接口

1
2
3
4
5
6
7
8
/**
* Created by wukewei on 16/6/19.
*/
public interface ICache {
<T> Observable<T> get(String key, Class<T> cls);
<T> void put(String key, T t);
}

2.内存存储的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* Created by wukewei on 16/6/19.
*/
public class MemoryCache implements ICache{
private LruCache<String, String> mCache;
public MemoryCache() {
final int maxMemory = (int) Runtime.getRuntime().maxMemory();
final int cacheSize = maxMemory / 8;
mCache = new LruCache<String, String>(cacheSize) {
@Override
protected int sizeOf(String key, String value) {
try {
return value.getBytes("UTF-8").length;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return value.getBytes().length;
}
}
};
}
@Override
public <T> Observable<T> get(final String key, final Class<T> cls) {
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
String result = mCache.get(key);
if (subscriber.isUnsubscribed()) {
return;
}
if (TextUtils.isEmpty(result)) {
subscriber.onNext(null);
} else {
T t = new Gson().fromJson(result, cls);
subscriber.onNext(t);
}
subscriber.onCompleted();
}
});
}
@Override
public <T> void put(String key, T t) {
if (null != t) {
mCache.put(key, new Gson().toJson(t));
}
}
public void clearMemory(String key) {
mCache.remove(key);
}
}

磁盘存储的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/**
* Created by wukewei on 16/6/19.
*/
public class DiskCache implements ICache{
private static final String NAME = ".db";
public static long OTHER_CACHE_TIME = 10 * 60 * 1000;
public static long WIFI_CACHE_TIME = 30 * 60 * 1000;
File fileDir;
public DiskCache() {
fileDir = CacheLoader.getApplication().getCacheDir();
}
@Override
public <T> Observable<T> get(final String key, final Class<T> cls) {
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
T t = (T) getDiskData1(key + NAME);
if (subscriber.isUnsubscribed()) {
return;
}
if (t == null) {
subscriber.onNext(null);
} else {
subscriber.onNext(t);
}
subscriber.onCompleted();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
@Override
public <T> void put(final String key, final T t) {
Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
boolean isSuccess = isSave(key + NAME, t);
if (!subscriber.isUnsubscribed() && isSuccess) {
subscriber.onNext(t);
subscriber.onCompleted();
}
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
}
/**
* 保存数据
*/
private <T> boolean isSave(String fileName, T t) {
File file = new File(fileDir, fileName);
ObjectOutputStream objectOut = null;
boolean isSuccess = false;
try {
FileOutputStream out = new FileOutputStream(file);
objectOut = new ObjectOutputStream(out);
objectOut.writeObject(t);
objectOut.flush();
isSuccess=true;
} catch (IOException e) {
Log.e("写入缓存错误",e.getMessage());
} catch (Exception e) {
Log.e("写入缓存错误",e.getMessage());
} finally {
closeSilently(objectOut);
}
return isSuccess;
}
/**
* 获取保存的数据
*/
private Object getDiskData1(String fileName) {
File file = new File(fileDir, fileName);
if (isCacheDataFailure(file)) {
return null;
}
if (!file.exists()) {
return null;
}
Object o = null;
ObjectInputStream read = null;
try {
read = new ObjectInputStream(new FileInputStream(file));
o = read.readObject();
} catch (StreamCorruptedException e) {
Log.e("读取错误", e.getMessage());
} catch (IOException e) {
Log.e("读取错误", e.getMessage());
} catch (ClassNotFoundException e) {
Log.e("错误", e.getMessage());
} finally {
closeSilently(read);
}
return o;
}
private void closeSilently(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (Exception ignored) {
}
}
}
/**
* 判断缓存是否已经失效
*/
private boolean isCacheDataFailure(File dataFile) {
if (!dataFile.exists()) {
return false;
}
long existTime = System.currentTimeMillis() - dataFile.lastModified();
boolean failure = false;
if (NetWorkUtil.getNetworkType(CacheLoader.getApplication()) == NetWorkUtil.NETTYPE_WIFI) {
failure = existTime > WIFI_CACHE_TIME ? true : false;
} else {
failure = existTime > OTHER_CACHE_TIME ? true : false;
}
return failure;
}
public void clearDisk(String key) {
File file = new File(fileDir, key + NAME);
if (file.exists()) file.delete();
}
}

isCacheDataFailure()方式中就是判断当前的数据是否失效,我是根据当前的网络状况来分wifi状况和非wifi状况,wifi状态下数据过期时间比较短,其他状态过期时间比较长。

CacheLoader的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
**
* Created by wukewei on 16/6/19.
*/
public class CacheLoader {
private static Application application;
public static Application getApplication() {
return application;
}
private ICache mMemoryCache, mDiskCache;
private CacheLoader() {
mMemoryCache = new MemoryCache();
mDiskCache = new DiskCache();
}
private static CacheLoader loader;
public static CacheLoader getInstance(Context context) {
application = (Application) context.getApplicationContext();
if (loader == null) {
synchronized (CacheLoader.class) {
if (loader == null) {
loader = new CacheLoader();
}
}
}
return loader;
}
public <T> Observable<T> asDataObservable(String key, Class<T> cls, NetworkCache<T> networkCache) {
Observable observable = Observable.concat(
memory(key, cls),
disk(key, cls),
network(key, cls, networkCache))
.first(new Func1<T, Boolean>() {
@Override
public Boolean call(T t) {
return t != null;
}
});
return observable;
}
private <T> Observable<T> memory(String key, Class<T> cls) {
return mMemoryCache.get(key, cls).doOnNext(new Action1<T>() {
@Override
public void call(T t) {
if (null != t) {
Log.d("我是来自内存","我是来自内存");
}
}
});
}
private <T> Observable<T> disk(final String key, Class<T> cls) {
return mDiskCache.get(key, cls)
.doOnNext(new Action1<T>() {
@Override
public void call(T t) {
if (null != t) {
Log.d("我是来自磁盘","我是来自磁盘");
mMemoryCache.put(key, t);
}
}
});
}
private <T> Observable<T> network(final String key, Class<T> cls
, NetworkCache<T> networkCache) {
return networkCache.get(key, cls)
.doOnNext(new Action1<T>() {
@Override
public void call(T t) {
if (null != t) {
Log.d("我是来自网络","我是来自网络");
mDiskCache.put(key, t);
mMemoryCache.put(key, t);
}
}
});
}
public void clearMemory(String key) {
((MemoryCache)mMemoryCache).clearMemory(key);
}
public void clearMemoryDisk(String key) {
((MemoryCache)mMemoryCache).clearMemory(key);
((DiskCache)mDiskCache).clearDisk(key);
}
}

网络获取的NetworkCache

1
2
3
4
5
6
/**
* Created by wukewei on 16/6/19.
*/
public abstract class NetworkCache<T> {
public abstract Observable<T> get(String key, final Class<T> cls);
}

6.接下来看怎么使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* Created by wukewei on 16/5/30.
*/
public class ItemPresenter extends BasePresenter<ItemContract.View> implements ItemContract.Presenter {
private static final String key = "new_list";
protected int pn = 1;
protected void replacePn() {
pn = 1;
}
private boolean isRefresh() {
return pn == 1;
}
private NetworkCache<ListPopular> networkCache;
public ItemPresenter(Activity activity, ItemContract.View view) {
super(activity, view);
}
@Override
public void getListData(String type) {
if (isRefresh()) mView.showLoading();
networkCache = new NetworkCache<ListPopular>() {
@Override
public Observable<ListPopular> get(String key, Class<ListPopular> cls) {
return mHotApi.getPopular(ItemPresenter.this.pn, Constants.PAGE_SIZE, type)
.compose(SchedulersCompat.applyIoSchedulers())
.compose(RxResultHelper.handleResult())
.flatMap(populars -> {
ListPopular popular = new ListPopular(populars);
return Observable.just(popular);
});
}
};
Subscription subscription = CacheLoader.getInstance(mActivity)
.asDataObservable(key + type + ItemPresenter.this.pn, ListPopular.class, networkCache)
.map(listPopular -> listPopular.data)
.subscribe(populars -> {
mView.showContent();
if (isRefresh()) {
if (populars.size() == 0) mView.showNotdata();
mView.addRefreshData(populars);
} else {
mView.addLoadMoreData(populars);
}
}, throwable -> {
if (isRefresh())
mView.showError(ErrorHanding.handleError(throwable));
handleError(throwable);
});
addSubscrebe(subscription);
}
}

一定要给个key,我是根据key来获取数据的,还要就是给个类型。但是这个我设计的这个缓存还是不是很理想,接来下想要实现的就是在传入的时候类的class都不用给明,要是有好的实现的方式,欢迎告诉我。

使用BehaviorSubject

BehaviorSubject的实现方法,废话不多说直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
* Created by wukewei on 16/6/20.
*/
public class BehaviorSubjectFragment extends BaseFragment {
public static BehaviorSubjectFragment newInstance() {
BehaviorSubjectFragment fragment = new BehaviorSubjectFragment();
return fragment;
}
String diskData = null;
String networkData = "从服务器获取的数据";
BehaviorSubject<String> cache;
View mView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mView = inflater.inflate(R.layout.fragment_content, container, false);
init();
return mView;
}
private void init() {
mView.findViewById(R.id.get).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
subscriptionData(new Observer<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onNext(String s) {
Log.d("onNext", s);
}
});
}
});
mView.findViewById(R.id.memory).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
BehaviorSubjectFragment.this.cache = null;
}
});
mView.findViewById(R.id.memory_disk).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
BehaviorSubjectFragment.this.cache = null;
BehaviorSubjectFragment.this.diskData = null;
}
});
}
private void loadNewWork() {
Observable<String> o = Observable.just(networkData)
.doOnNext(new Action1<String>() {
@Override
public void call(String s) {
BehaviorSubjectFragment.this.diskData = s;
Log.d("写入磁盘", "写入磁盘");
}
});
o.subscribe(new Action1<String>() {
@Override
public void call(String s) {
cache.onNext(s);
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
}
});
}
private Subscription subscriptionData(@NonNull Observer<String> observer) {
if (cache == null) {
cache = BehaviorSubject.create();
Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> subscriber) {
String data = diskData;
if (data == null) {
Log.d("来自网络", "来自网络");
loadNewWork();
} else {
Log.d("来自磁盘", "来自磁盘");
subscriber.onNext(data);
}
}
})
.subscribeOn(Schedulers.io())
.subscribe(cache);
} else {
Log.d("来自内存", "来自内存");
}
return cache.observeOn(AndroidSchedulers.mainThread()).subscribe(observer);
}
}

其中最主要的是subscriptionData()这个方法,就是先判断 cache是否存在要是存在的话就返回内存中数据,再去判断磁盘数据是否存在,如果存在就返回,要是前面两种都不存在的时候,再去网络中获取数据。还有最重要的是当你从网络获取数据的时候要记得保存在内存中和保存在磁盘中,在磁盘获取数据的时候把它赋值给内存。

MVP+Rxjava+Retrofit构建项目

发表于 2017-10-03

第一次写文章,不好之处还请谅解。2015最值得android程序猿去学习的就是rxjava了,关于rxjava就不再多描述了。本文是介绍怎么使用mvp+rxjava+retrofit来构建一个新的项目,项目采用mvp的方式,参考了google的官方mvp项目。Hot是关于微信头条分享的app,项目地址

项目介绍

BaseActivity的设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Created by wukewei on 16/5/26.
*/
public abstract class BaseActivity<T extends IPresenter> extends AppCompatActivity {
protected T mPresenter;
protected Activity mContext;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayout());
ButterKnife.bind(this);
mContext = this;
mPresenter = getPresenter();
initEventAndData();
}
protected void setCommonBackToolBack(Toolbar toolbar, String title) {
toolbar.setTitle(title);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
toolbar.setNavigationOnClickListener(v -> onBackPressed());
}
@Override
protected void onDestroy() {
super.onDestroy();
ButterKnife.unbind(this);
if (mPresenter != null) mPresenter.detachView();
}
protected abstract T getPresenter();
protected abstract int getLayout();
protected abstract void initEventAndData();
}

最主要的设置了presenter的泛型,并且提供了初始化的函数 protected abstract T getPresenter();。

BasePresenter的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Created by wukewei on 16/5/26.
*/
public abstract class BasePresenter<T extends IView> implements IPresenter {
protected Activity mActivity;
protected T mView;
protected CompositeSubscription mCompositeSubscription;
// protected static final HotApi mHotApi = HotFactory.getHotApi();
public BasePresenter(Activity activity, T view) {
this.mActivity = activity;
this.mView = view;
}
protected void handleError(Throwable throwable) {
ToastUtil.showShort(mActivity, ErrorHanding.handleError(throwable));
}
protected void unSubscribe() {
if (mCompositeSubscription != null) {
mCompositeSubscription.unsubscribe();
}
}
protected void addSubscrebe(Subscription subscription) {
if (mCompositeSubscription == null) {
mCompositeSubscription = new CompositeSubscription();
}
mCompositeSubscription.add(subscription);
}
@Override
public void detachView() {
this.mView = null;
unSubscribe();
}
}

也采用了范型来绑定view,使用了CompositeSubscription来进行避免内存的泄漏。

Api的设计

在实际情况中每个公司的api的设计都是不一样的,我采用如下的设计形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Created by wukewei on 16/5/26.
*/
public class ApiResponse<T> {
public static final int SUCCESS_CODE = 200;
private int code;
private String msg;
private T newslist;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getNewsList() {
return newslist;
}
public void setNewsList(T newsList) {
this.newslist = newsList;
}
public boolean isSuccess() {
if (this.code == SUCCESS_CODE) {
return true;
} else {
return false;
}
}
}

你们可以根据自己公司的实际情况修改返回的成功码。

Rxjava的一些设计:

大家都知道rxjava就是能灵活的在线程之间进行切换,在使用的时候我使用了Transformer操作符。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Created by wukewei on 16/5/26.
*/
public class SchedulersCompat {
private final static Observable.Transformer ioTransformer = o -> o.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
public static <T> Observable.Transformer<T, T> applyIoSchedulers() {
return (Observable.Transformer<T, T>) ioTransformer;
}
}

在数据处理的时候,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Created by wukewei on 16/5/26.
*/
public class RxResultHelper {
public static <T> Observable.Transformer<ApiResponse<T>, T> handleResult() {
return apiResponseObservable -> apiResponseObservable.flatMap((Func1<ApiResponse<T>, Observable<T>>) tApiResponse -> {
if (tApiResponse.isSuccess()) {
//表示成功
return createData(tApiResponse.getNewsList());
} else {
return Observable.error(new ServerException(tApiResponse.getMsg()));
}
});
}
public static <T> Observable<T> createData(T t) {
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
try {
subscriber.onNext(t);
subscriber.onCompleted();
} catch (Exception e) {
subscriber.onError(e);
}
}
});
}
}

在这里你可以根据自己的实际情况来添加,大部分app都是有token一说,你可以在这里判断当token过期的时候可以跳到登录界面。
在项目中的时候就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Subscription subscription = mHotApi.getPopular(this.pn, Constants.PAGE_SIZE, type)
.compose(SchedulersCompat.applyIoSchedulers())
.compose(RxResultHelper.handleResult())
.subscribe(populars -> {
mView.showContent();
if (isRefresh()) {
if (populars.size() == 0) mView.showNotdata();
mView.addRefreshData(populars);
} else {
mView.addLoadMoreData(populars);
}
}, throwable -> {
mView.showError(ErrorHanding.handleError(throwable));
handleError(throwable);
});
addSubscrebe(subscription);

是不是觉得使用起来非常的爽啊,还有一个就是在实际情况中很多一部分请求要设置一些数据好比来自android的还是ios的,或者添加一些token在请求中,本项目没有使用token,但是有个apikey代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
**
* Created by wukewei on 16/5/26.
*/
public class OkHttpManager {
private static OkHttpClient mOkHttpClient;
public static OkHttpClient getInstance() {
if (mOkHttpClient == null) {
synchronized (OkHttpManager.class) {
if (mOkHttpClient == null) {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
Interceptor apikey = chain -> chain.proceed(chain.request().newBuilder()
.addHeader("apikey", Constants.Api_Key).build());
File cacheFile = new File(App.getAppContext().getCacheDir(), "cache");
Cache cache = new Cache(cacheFile, 1024 * 1024 * 100); //100Mb
mOkHttpClient = new OkHttpClient.Builder()
.readTimeout(Constants.HTTP_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.connectTimeout(Constants.HTTP_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
.addInterceptor(apikey)
.addInterceptor(loggingInterceptor)
.addNetworkInterceptor(new HttpCacheInterceptor())
.cache(cache)
.build();
}
}
}
return mOkHttpClient;
}
static class HttpCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (!NetWorkUtil.isNetConnected(App.getAppContext())) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
Log.d("Okhttp", "no network");
}
Response originalResponse = chain.proceed(request);
if (NetWorkUtil.isNetConnected(App.getAppContext())) {
//有网的时候读接口上的@Headers里的配置,你可以在这里进行统一的设置
String cacheControl = request.cacheControl().toString();
return originalResponse.newBuilder()
.header("Cache-Control", cacheControl)
.removeHeader("Pragma")
.build();
} else {
return originalResponse.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=2419200")
.removeHeader("Pragma")
.build();
}
}
}
}

你可以在获取本地token的时候,当有的时候加上即可。

7月13日更新:DataManager

网上有很多在mvp的时候关于数据层的设计,我这边是添加了DataManager来管理app数据,在p层是不关心数据的来源,无论是网络的还是本地缓存的数据,这个设计参考了别的大神的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
* Created by wukewei on 16/7/12.
* 这个类是管理app的数据来源无论从网络获取.内存.还是磁盘
*/
public class DataManager {
private static Gson gson;
private static RxBus rxBus;
private static HotApi mHotApi;
private static CacheLoader cacheLoader;
private Handler mHandler;
private DataManager() {}
public static DataManager getInstance() {
return DataManagerHolder.INSTANCE;
}
public static class DataManagerHolder {
private final static DataManager INSTANCE = new DataManager();
}
public void initService() {
gson = new Gson();
rxBus = RxBus.getDefault();
cacheLoader = CacheLoader.getInstance(App.getAppContext());
HandlerThread ioThread = new HandlerThread("IoThread");
ioThread.start();
mHandler = new Handler(ioThread.getLooper());
mHandler.post(new Runnable() {
@Override
public void run() {
mHotApi = HotFactory.getHotApi();
}
});
}
/***
* 获取分类的类型
* @param
* @param
* @return
*/
public List<String> getTabs() {
List<String> tabs = new ArrayList<>();
tabs.add("科技");
tabs.add("美女");
tabs.add("生活");
tabs.add("娱乐");
tabs.add("搞笑");
tabs.add("宅男");
return tabs;
}
/***
* 获取列表
* @param pn 页码
* @param type 类别名称
* @return
*/
public Observable<List<Popular>> getPopular(int pn, String type) {
return mHotApi.getPopular(pn, Constants.PAGE_SIZE, type)
.compose(SchedulersCompat.applyIoSchedulers())
.compose(RxResultHelper.handleResult())
.doOnNext(populars -> {
if (pn == 1) {
ListPopular popular = new ListPopular(populars);
cacheLoader.upNewData(type, popular);
}
});
}
/***
* 获取缓存信息 默认只缓存第一页
* @param type 类别名称
* @param
* @return
*/
public Observable<List<Popular>> getCachePopular(String type) {
NetworkCache<ListPopular> networkCache = new NetworkCache<ListPopular>() {
@Override
public Observable<ListPopular> get(String key, Class<ListPopular> cls) {
return mHotApi.getPopular(1, Constants.PAGE_SIZE, type)
.compose(SchedulersCompat.applyIoSchedulers())
.compose(RxResultHelper.handleResult())
.flatMap(populars -> {
ListPopular popular = new ListPopular(populars);
return Observable.just(popular);
});
}
};
return cacheLoader.asDataObservable(Constants.NEW_LIST + type, ListPopular.class, networkCache)
.map(listPopular -> listPopular.data);
}
}

记得在app的时候初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class App extends Application {
private static App appContext;
@Override
public void onCreate() {
super.onCreate();
appContext = this;
DataManager.getInstance().initService();
}
public static App getAppContext() {
return appContext;
}
}

这样在p中的使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Subscription subscription = DataManager.getInstance().getPopular(pn, type)
.subscribe(populars -> {
mView.showContent();
if (isRefresh()) {
if (populars.size() == 0) mView.showNotdata();
mView.addRefreshData(populars);
} else {
mView.addLoadMoreData(populars);
}
}, throwable -> {
if (isRefresh())
mView.showError(ErrorHanding.handleError(throwable));
handleError(throwable);
});

结束

通过这次的实践加深了我对mvp的认知,本人对rxjava认识不够深入,还在不断的学习中,要是有什么错误之处还望指正。

Hello World

发表于 2017-10-03

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

12
GoGo (吴克伟)

GoGo (吴克伟)

20 日志
10 标签
GitHub 简书
© 2019 GoGo (吴克伟)
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.3