0%


由于我是android开发过渡到学习react-native的,所以对于一些web的状态管理方案不是很了解,比如说遇到多界面的异步刷新,直接使用回调的方式显得相当麻烦,到处留下接口,导致项目结构混乱,但在android中只需要使用广播,eventbus或者rxbus就可以解决的问题。我了解到react-redux这个东西,就是一个状态管理的解决方案,后来我发现,更多人推荐使用mobx去替换react-redux因为这个东西使用更简洁,更易于上手。当一个项目越来越庞大的时候,我们发现界面存在很多公共的逻辑,是不需要重复去写的,封装避免冗余代码这个工作是必不可少的,对比android,我们可以使用BaseActivity来封装公共逻辑,在react中最初我了解到mixin这个东西可以实现公共逻辑的封装,但是mixin不支持es6,所以最后采用了高阶组件的方式封装组件的base层,执行组件的公共逻辑。接下来详细介绍实现过程。


mobx基础

当我第一次看到mobx的时候我觉得它就是观察者模式,和android中的mvvm架构实现是差不多的。

mobx术语

主要是4个,可观察的状态,计算值,状态反应,触发动作

定义数据类

可观察的状态

通过@observable去标记变量,如对象,数组等,添加了可观察的功能。

1
2
@observable count;
@observable name;

根据状态得到计算值

1
2
3
@computed get msg() {
return `${this.name} say count is ${this.count}`;
}

状态变化时的反应

autorun 方法需要传入数据类的实例,在可观察的变量被修改时调用

1
2
3
autorun(() => {
console.log(appState.msg);
});

触发修改状态的动作

通过定义的动作方法修改状态的值

1
2
3
4
5
6
@action add() {
this.count += 1;
}
@action changeName(name) {
this.name = name;
}

整个数据类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default class AppState {
@observable count;
@observable name;
@computed get msg() {
return `${this.name} say count is ${this.count}`;
}
@action add() {
this.count += 1;
}
@action changeName(name) {
this.name = name;
}

// 返回json格式化数据
toJson() {
return {
count: this.count,
name: this.name,
}
}
}

在组件中使用

在index中注入数据类的实例对象,通过props传入到内部组件

1
2
3
4
5
6
7
8
9
10
import AppState from './store/app-state';
class Root extends React.Component {
render() {
return (
<Provider appState={new AppState()}>
<App />
</Provider>
);
}
}

在组件内监听,用@observer标记组件,@inject(‘appState’)注入数据类,通过props获取到数据类实例

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
import React from 'react'
import {
observer,
inject,
} from 'mobx-react';
import PropTypes from 'prop-types';
import { AppState } from '../../store/app-state';

@inject('appState') @observer
export default class TopicList extends React.Component {
constructor() {
super();
this.changeName = this.changeName.bind(this);
}

componentDidMount() {
// do something here

}

changeName(event) {
this.props.appState.changeName(event.target.value);
}

render() {
return (
<div>
<input type="text" onChange={this.changeName} />
<span>{this.props.appState.msg}</span>
</div>
);
}
}
TopicList.propTypes = {
appState: PropTypes.instanceOf(AppState),
};

利用高阶组件封装组件的base层

高阶组件Higher Order Component简称HOC,是一个使用函数来实现的类工厂,接收一个React.Component的参数,这个参数必须传,我们可以定义为WrappedComponent,代表被包裹的组件,返回一个React.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
export const HighOrderComponent = (WrappedComponent, title) => {
return class HOC extends React.Component {
// 公共state初始化
constructor(props) {
super(props);
this.state = {
username: ''
}
}

// 公共逻辑处理
componentWillMount() {
let username = localStorage.getItem('username');
this.setState({
username: username
});
}

// 界面渲染的公共部分
render() {
return(
<div>
<View>{title}</View>
<WrappedComponent {...this.props} username={this.state.username}></WrappedComponent>
</div>
)
}
}
}

我们可以在这个类中操作props,对props的增删改查,可以通过Refs访问到组件实例,对state做处理,也可以用其他的组件包裹WrappedComponent。作为组件的base层,在这里处理公共逻辑,使用的时候传入被包裹的组件。

1
2
3
4
5
6
7
8
9
10
class Index extends React.Component {
render() {
return(
<View>
<Text>Hi {this.props.username}</Text>
</View>
)
}
}
export const HighOrderIndex = HighOrderComponent(Index, 'Index title');


最近又是很长一段时间没写文章了,忙着学习webpack,react,react-router,eslint,babel,mobx,一系列框架,开源的SimpleOne项目也在持续维护,把以前的ES5语法改为ES6,移除了react-timer-mixin这个模块,当然这个项目中的某些技术点需要总结,接下来的几篇会对这些技术点做一个记录。我们的app经常会用到长列表的展示方式,显示内容过多,大多数情况会用到分页加载,滑动到列表的底部,通过加载更多的方式,显示下一页的内容,看起来是一页,其实是多页拼接的长列表。一般我们的列表都有下拉刷新,但是这些react-native没有封装好的组件,我们需要自己实现,下面详细记录实现过程,如果你有更好的实现方式,希望一起学习讨论。


当前的效果图gif:
效果图1
效果图2

要实现下拉刷新和加载更多,需要对scrollview做一个扩展,对scrollview的滑动事件做监听和逻辑处理,利用官方的PanResponder 做手势识别,显示一个自定义的顶部下拉刷新视图,所以我们可以利用react-native现有的组件封装一个支持下拉刷新和加载更多的view来实现我们的需求。这里我们定义下拉刷新的动画是一个常见的图片旋转+文字提示。

对ScrollView封装和使用

封装ScrollView实现下拉刷新和加载更多

首先,定义下拉到位时距离顶部的高度,默认下拉到位的时长,默认顶部刷新视图的高度,下拉刷新提示文字的显示状态,主要是4个状态,默认状态,正在下拉状态,下拉到位状态,和下拉释放状态。

1
2
3
4
5
6
7
8
9
const defaultDuration = 300; //默认时长
/**
* 提示文字的显示状态
* @type {{pulling: boolean, pullok: boolean, pullrelease: boolean}}
*/
const defaultState = {pulling: false, pullok: false, pullrelease: false}; //默认状态
const statePulling = {pulling: true, pullok: false, pullrelease: false}; //正在下拉状态
const statePullok = {pulling: false, pullok: true, pullrelease: false}; //下拉到位状态
const statePullrelease = {pulling: false, pullok: false, pullrelease: true}; //下拉释放状态

接下来是定义手势判断

1
2
3
4
5
6
7
8
9
10
11
12
//向下手势
const isDownGesture = (x, y) => {
return y > 0 && (y > Math.abs(x));
};
//向上手势
const isUpGesture = (x, y) => {
return y < 0 && (Math.abs(x) < Math.abs(y));
};
//垂直方向手势
const isVerticalGesture = (x, y) => {
return (Math.abs(x) < Math.abs(y));
};

定义我们自定义的组件PullScollView继承自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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static defaultProps = {
topRefreshHeight: 50, //顶部刷新视图的高度
pullOkMargin:100 //下拉到位状态时距离顶部的高度
}

static propTypes = {
topRefreshHeight: PropTypes.number,
pullOkMargin: PropTypes.number,
onPulling: PropTypes.func,
onPullOk: PropTypes.func,
onPullRelease: PropTypes.func,
onLoadMore: PropTypes.func,
onScroll: PropTypes.func,
children: PropTypes.array,
style: PropTypes.object,
loadMoreState: PropTypes.number,
onRetry: PropTypes.func,
}

constructor(props) {
super(props)
this.defaultScrollEnabled = false
this.topRefreshHeight = this.props.topRefreshHeight
this.defaultXY = {x: 0, y: this.topRefreshHeight * -1}
this.pullOkMargin = this.props.pullOkMargin
this.state = Object.assign({}, props, {
pullPan: new Animated.ValueXY(this.defaultXY), //下拉区域
scrollEnabled: this.defaultScrollEnabled, //滚动启动
height: 0,
})
// 滚动监听
this.onScroll = this.onScroll.bind(this)
// 布局监听
this.onLayout = this.onLayout.bind(this)
// 下拉状态监听
this.isPullState = this.isPullState.bind(this)
// 下拉未到位,重置回调
this.resetDefaultXYHandler = this.resetDefaultXYHandler.bind(this)
// 数据加载完,重置归位
this.resolveHandler = this.resolveHandler.bind(this)
// 顶部绘制
this.renderTopRefresh = this.renderTopRefresh.bind(this)
// 默认顶部绘制
this.defaultTopRefreshRender = this.defaultTopRefreshRender.bind(this)
// 多点触摸识别
this.panResponder = PanResponder.create({
onMoveShouldSetPanResponder: this.onShouldSetPanResponder.bind(this),
onPanResponderMove: this.onPanResponderMove.bind(this),// 最近一次的移动距离为gestureState.move{X,Y}
onPanResponderRelease: this.onPanResponderRelease.bind(this),//放开了所有的触摸点,且此时视图已经成为了响应者。
})
this.setPullState(defaultState)// 设置提示文字的默认状态

}

PanResponder手势响应回调中的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
// 手势响应回调,是否处理
onShouldSetPanResponder(e, gesture) {
//非垂直手势不处理
if (!isVerticalGesture(gesture.dx, gesture.dy)) {
return false;
}
if (!this.state.scrollEnabled) {
this.lastY = this.state.pullPan.y._value;
return true;
} else {
return false;
}
}

PanResponder手势移动回调中的处理

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
// 手势移动的处理
onPanResponderMove(e, gesture) {
if (isUpGesture(gesture.dx, gesture.dy)) { //向上手势
// 如果处于下拉状态,重置
if(this.isPullState()) {
this.resetDefaultXYHandler();
} else { // 恢复到默认位置
this.scroll.scrollTo({x:0, y: gesture.dy * -1});
}
return;
} else if (isDownGesture(gesture.dx, gesture.dy)) { //向下手势
// 设置下拉区域
this.state.pullPan.setValue({x: this.defaultXY.x, y: this.lastY + gesture.dy / 2});
if (gesture.dy < this.topRefreshHeight + this.pullOkMargin) { //正在下拉
// 之前状态不是正在下拉状态,调用正在下拉回调
if (!this.curState.pulling) {
this.props.onPulling && this.props.onPulling();
}
// 记录当前为下拉状态
this.setPullState(statePulling);
} else { //下拉到位
// 之前状态不是下拉到位状态,调用下拉到位回调
if (!this.state.pullok) {
this.props.onPullOk && this.props.onPullOk();
}
// 记录当前为下拉到位状态
this.setPullState(statePullok);
}
}
}

PanResponder手势释放回调中的处理

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
// 手势释放
onPanResponderRelease() {
if (this.curState.pulling) { // 之前是下拉状态
this.resetDefaultXYHandler(); //重置状态
}
if (this.curState.pullok) { // 之前是下拉到位状态
// 之前没有松开
if (!this.curState.pullrelease) {
// 之前是刷新中,调用数据处理回调
if (this.props.onPullRelease) {
this.props.onPullRelease(this.resolveHandler);
} else { // 重置
setTimeout(() => {this.resetDefaultXYHandler()}, 3000);
}
}
this.setPullState(statePullrelease); //记录当前状态为完成下拉,已松开
// 刷新view 的下拉动画开启
Animated.timing(this.state.pullPan, {
toValue: {x: 0, y: 0},
easing: Easing.linear,
duration: defaultDuration
}).start();

// 刷新视图中的转圈动画开启
this.spin();
}
}

下拉重置,注意这里需要停止转圈的动画,根据当前剩余的变化值去初始化动画,如果没有这个操作,可能导致后面几次下拉刷新显示的刷新视图动画异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/** 数据加载完成后调用此方法进行重置归位*/
resolveHandler() {
if (this.curState.pullrelease) { //仅触摸松开时才触发
this.resetDefaultXYHandler();
}
}

// 重置下拉
resetDefaultXYHandler() {
this.curState = defaultState;
this.state.pullPan.setValue(this.defaultXY);
this.state.spinValue.stopAnimation(value => {
console.log('剩余时间' + (1 - value) * 3000);
//计算角度比例
this.animation = Animated.timing(this.state.spinValue, {
toValue: 1,
duration: (1 - value) * 3000,
easing: Easing.linear,
});
});
}

实现刷新视图中的循环转圈动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 转圈动画
spin () {
this.animation.start((result)=>{
//正常转完1周,继续转
if (Boolean(result.finished)) {
this.animation = Animated.timing(
this.state.spinValue,
{
toValue: 1,
duration: 3000,
easing: Easing.linear
}
);
this.state.spinValue.setValue(0);// 每次转完都要重置
this.spin();
}
});
}

我们仍然需要对scrollview的滑动事件做一个监听,这里我们需要过滤一个临界状态,当列表已经滑动到顶部,但还没有触发下拉刷新的状态,不允许继续滚动。滑动监听实现加载更多,先获取到垂直方向的滑动距离,列表的高度,内容的高度,当滑出去的距离加上列表的高度大于等于内容高度时触发加载更多,这里给个20的偏移量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// scrollview 的滚动回调
onScroll(e) {
if (e.nativeEvent.contentOffset.y <= 0) { //临界状态,此时已经到列表顶部,但是还没触发下拉刷新状态
this.setState({scrollEnabled: this.defaultScrollEnabled})
} else if (!this.isPullState()) { //当前不是下拉状态允许滚动
this.setState({scrollEnabled: true})
}
let y = e.nativeEvent.contentOffset.y;
// console.log('滑动距离' + y);
let height = e.nativeEvent.layoutMeasurement.height;
// console.log('列表高度' + height);
let contentHeight = e.nativeEvent.contentSize.height;
// console.log('内容高度' + contentHeight);
// console.log('判断条件' + (y + height));
if (y + height >= contentHeight - 20) {
console.log('触发加载更多');
this.props.onLoadMore && this.props.onLoadMore()
}
// 调用外部的滑动回调
this.props.onScroll && this.props.onScroll(e)
}

处于下拉过程判断

1
2
3
4
//处于下拉过程判断
isPullState() {
return this.curState.pulling || this.curState.pullok || this.curState.pullrelease;
}

设置当前处于的下拉状态

1
2
3
4
5
6
7
// 设置当前的下拉状态
setPullState(pullState) {
if (this.curState !== pullState) {
this.curState = pullState;
this.renderTopRefresh();
}
}

根据当前的下拉状态,完成下拉刷新部分的重绘:

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
// 绘制下拉刷新
renderTopRefresh() {
let { pulling, pullok, pullrelease } = this.curState;
return this.defaultTopRefreshRender(pulling, pullok, pullrelease);
}


/**
使用setNativeProps解决卡顿问题
绘制默认的下拉刷新
*/
defaultTopRefreshRender(pulling, pullok, pullrelease) {
//控制下拉刷新提示文字显示状态
setTimeout(() => {
if (pulling) {
this.txtPulling && this.txtPulling.setNativeProps({style: styles.show});
this.txtPullok && this.txtPullok.setNativeProps({style: styles.hide});
this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.hide});
} else if (pullok) {
this.txtPulling && this.txtPulling.setNativeProps({style: styles.hide});
this.txtPullok && this.txtPullok.setNativeProps({style: styles.show});
this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.hide});
} else if (pullrelease) {
this.txtPulling && this.txtPulling.setNativeProps({style: styles.hide});
this.txtPullok && this.txtPullok.setNativeProps({style: styles.hide});
this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.show});
}
}, 1);
// 初始化旋转角度
const spin = this.state.spinValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '1080deg']
});
// 绘制下拉刷新view
return (
<View style={{flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: defaultTopRefreshHeight}}>
<Animated.Image
style={{
width: 23,
height: 23,
transform: [{rotate: spin}] ,
position:'absolute',
left:24
}}
source={{uri: 'default_ptr_rotate'}}
/>

<Text ref={(c) => {this.txtPulling = c;}} style={styles.hide}>{tipText.pulling}</Text>
<Text ref={(c) => {this.txtPullok = c;}} style={styles.hide}>{tipText.pullok}</Text>
<Text ref={(c) => {this.txtPullrelease = c;}} style={styles.hide}>{tipText.pullrelease}</Text>
</View>
);
}

测量宽高,动态设置自定义view的宽高,绘制整个自定义view以及该组件内部包含的子view

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
// 获得测量的宽高,动态设置
onLayout(e) {
if (this.state.width !== e.nativeEvent.layout.width || this.state.height !== e.nativeEvent.layout.height) {
this.scrollContainer.setNativeProps({style: {width: e.nativeEvent.layout.width, height: e.nativeEvent.layout.height}});
this.width = e.nativeEvent.layout.width;
this.height = e.nativeEvent.layout.height;
}
}



render() {
return (
<View style={[Refresh.styles.wrap, this.props.style]} onLayout={this.onLayout}>
<Animated.View style={[this.state.pullPan.getLayout()]}>
{/*绘制下拉刷新view*/}
{this.renderTopRefresh()}
{/*绘制里面的子view*/}
<View ref={(c) => {
this.scrollContainer = c
}} {...this.panResponder.panHandlers} style={{width: this.state.width, height: this.state.height}}>
<ScrollView {...this.props} ref={(c) => {
this.scroll = c
}} scrollEnabled={this.state.scrollEnabled} onScroll={this.onScroll}>
{this.props.children}
{/*绘制加载view*/}
<LoadMore state={this.props.loadMoreState} onRetry={() => this.props.onRetry()}/>
</ScrollView>
</View>
</Animated.View>
</View>
)
}

设置默认提示文本和自带样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 默认提示文本
const tipText = {
pulling: "下拉刷新...",
pullok: "松开刷新......",
pullrelease: "刷新中......"
};

// 自带样式
const styles = StyleSheet.create({
wrap: {
flex: 1,
flexGrow: 1,
flexDirection: 'column',
zIndex:-999,
},
hide: {
position: 'absolute',
left: 10000
},
show: {
position: 'relative',
left: 0
}
});

在业务代码中使用

1
2
3
4
<PullScrollView style={{flex: 1, backgroundColor: 'white'}}
onPullRelease={this.onPullRelease}>
...
</PullScollView>

这里注意在onPullRelease中一定要调用resolve()让下拉刷新重置

1
2
3
4
5
6
onPullRelease(resolve) {
//刷新完毕,重置下拉刷新,再次更新刷新和加载更多状态
setTimeout(() => {
resolve();
}, 3000);
}

我们需要在触发加载更多,发起网络请求时显示一个加载更多的loadingview,通过loading参数传值控制当前是否显示,同样简单封装一下,这里加了一个逐个展示小圆点的组合动画

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
const pointsNum = 3; //点数量

class LoadMore extends Component {
constructor(props) {
super(props)
// 初始化点的数量
this.arr = []
for (let i = 0; i < pointsNum; i++) {
this.arr.push(i)
}
// 初始化动画
this.animatedValue = []
this.arr.forEach((value) => {
this.animatedValue[value] = new Animated.Value(0)
})
this.state = {
curState: this.props.state
}

}

componentDidMount(){
this.isMount = true
}

componentWillUnmount(){
this.isMount = false
}
/**
* 动画执行,逐个显示
*/
animate() {
this.arr.forEach((value) => {
this.animatedValue[value].setValue(0);
})
const animations = this.arr.map((item) => {
return Animated.timing(
this.animatedValue[item],
{
toValue: 1,
duration: 200,
easing: Easing.linear
}
)
})
this.loadingAnim = Animated.sequence(animations)
this.loadingAnim.start((result) => {
if (Boolean(result.finished)) this.animate()
})
}

render() {
return (
<View style={[styles.container]}>
{this.state.curState !== LoadMoreState.state.hide ? this.renderLoad() : null}
</View>
)
}


componentWillReceiveProps(nextProps) {
this.setState({
curState: nextProps.state
})
switch (nextProps.state) {
case LoadMoreState.state.loading:
if (this.props.loadAnim){
this.animate()
}
break
case LoadMoreState.state.hide:
this.arr.forEach((item) => {
this.animatedValue[item].stopAnimation(value => {
// console.log('剩余时间' + (1 - value) * 200);
});
})
break
case LoadMoreState.state.noMore:
setTimeout(() => {
if(this.isMount){
this.setState({
curState: LoadMoreState.state.hide
})
}
}, 3000)
break

}
}

/**
* 根据状态返回绘制文字
* @returns {string}
*/
renderLoadText() {
switch (this.state.curState) {
case LoadMoreState.state.loading:
if (this.props.loadAnim){
return LoadMoreState.stateText.loading
}else{
return LoadMoreState.stateText.loading + '...'
}

case LoadMoreState.state.noMore:
return LoadMoreState.stateText.noMore
case LoadMoreState.state.tip:
return LoadMoreState.stateText.tip
case LoadMoreState.state.error:
return LoadMoreState.stateText.error
}
}

// 绘制文字
renderLoad() {
const animations = this.arr.map((value, index) => {
return (
<Animated.Text key={index} style={{
opacity: this.animatedValue[value], fontSize: Config.screenW * 0.06, color: Config.normalTextColor
}}>.</Animated.Text>
)
})
return (
<TouchableOpacity
style={{flexDirection: 'row', alignItems: 'center', height: Config.screenW * 0.14,}}
onPress={() => new DoubleClick().filterDoubleClick(
function () {
if (this.state.curState === LoadMoreState.state.error){
this.props.onRetry && this.props.onRetry()
this.setState({
curState:LoadMoreState.state.loading
})
this.animate()
}
}.bind(this)
)}>
<Text style={{
color: Config.normalTextColor,
fontSize: Config.screenW * 0.04,
marginRight: Config.screenW * 0.02,
}}>
{this.renderLoadText()}
</Text>
{/*是加载状态绘制。。。的动画*/}
{this.state.curState === LoadMoreState.state.loading ?
<View style={styles.pointsView}>
{animations}
</View> : null}

</TouchableOpacity>
)
}
}

LoadMore.propTypes = {
state: PropTypes.number, //当前状态
onRetry: PropTypes.func ,//重试回调
loadAnim: PropTypes.bool, //是否显示加载动画,flatlist尾部组件不支持
}
LoadMore.defaultProps = {
state: LoadMoreState.state.hide, //默认不显示
loadAnim: false //默认不用动画
}

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
pointsView: {
height: Config.screenW * 0.14,
flexDirection: 'row',
alignItems: 'center',
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
})

export default LoadMore;

对flatlist封装

封装flatlist实现下拉刷新和加载更多

与scrollview的封装方式大致相同,render方法需要稍做修改

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
render() {
return (
<View style={[Refresh.styles.wrap, this.props.style]} onLayout={this.onLayout}>
<Animated.View style={[this.state.pullPan.getLayout()]}>
{/*绘制下拉刷新view*/}
{this.renderTopRefresh()}
{/*绘制里面的子view*/}
<View ref={(c) => {
this.scrollContainer = c
}} {...this.panResponder.panHandlers} style={{width: this.state.width, height: this.state.height}}>
<FlatList
ref={(c) => {
this.list = c
}}
extraData={this.state}
{...this.props}
onScroll={this.onScroll}
scrollEnabled={this.state.scrollEnabled}
ListFooterComponent={() => {
return <LoadMore state={this.props.loadMoreState} onRetry={() => this.props.onRetry()}/>
}}
/>

</View>
</Animated.View>
</View>
)
}

在业务代码中使用

1
2
3
4
5
6
7
8
9
10
11
12
<PullFlatList
data={this.state.data.data}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={this.space}
renderItem={({item, index}) => (
<ComicImg imgUrl={item} index={index}/>
)}
keyExtractor={item => item}
numColumns={1}
onPullRelease={this.props.onRefresh}
style={[CommonStyle.styles.listView,{width: Config.screenW,height:Config.screenH}]}
/>

对应的源码地址:

组件封装
组件使用


随着混合开发越来越火,逐渐成为行业发展趋势,ReactNative也一直保持着快速更新的节奏,”learn once,write anywhere”这个宣传语一直吸引着我,公司最近也不忙了,一直想入坑的我终于找到了一个合适的时间点,很长一段时间没有更新博客了,因为在学习ReactNative这个框架,由于喜欢one一个的ui界面,做了这个开源项目,这是一个高仿韩寒one一个的开源项目,在此声明,本项目完全以学习为目的,不侵犯one一个的利益,遵守开源协议,所有接口全部来源于one一个android版4.3.4抓包获得,项目中使用的素材也是解压apk文件获得,不打算支持登录后的评论, 分享, 点赞操作, 原版是使用android原生实现的,本项目用ReactNative框架实现,还使用了ReactNative的第三方库,sdk接入是调用了原生android sdk模块,此项目目前只对android进行了适配,ios目前尚未适配,项目结构并非标准的ReactNative项目结构,而是android原生项目结构引入ReactNative框架的混合开发结构。我学习ReactNative不久,以前是做android原生开发的,如果有写的不好的地方,或者是你有更好的实现方式,希望一起学习讨论。


先上目前的效果图:

如何运行

确保你的编译设备和运行设备在同一网络下,并且配置开发设置中的主机ip和端口号(Dev settings -> Debug server host & port for device)

  1. 在命令行输入, react-native start
  2. 选择你的运行设备, run app

api接口清单

这里对提取出的api接口做个记录,本人以学习为目的,希望读者也不要用于任何商业项目中,目前列出的接口是基于4.3.4的android版:

  1. one首页(第一页,date=0,更多页date=yyyy-MM-dd)

    http://v3.wufazhuce.com:8000/api/channel/one/{date}/0

  2. 专题列表(第一页,last_id=0,更多页last_id=上次请求结束的id)

    http://v3.wufazhuce.com:8000/api/banner/list/4?last_id={last_id}

  3. 横着的列表(所有人问所有人)

    http://v3.wufazhuce.com:8000/api/banner/list/5

  4. 近期热门作者

    http://v3.wufazhuce.com:8000/api/author/hot

  5. one首页菜单跳转

    • 问答
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/question/{content_id}
      http://v3.wufazhuce.com:8000/api/question/htmlcontent/{content_id}

    • 连载
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/serial/{content_id}/0
      http://v3.wufazhuce.com:8000/api/serialcontent/htmlcontent/{content_id}

    • 音乐
      http://v3.wufazhuce.com:8000/api/music/htmlcontent/{content_id}
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/music/{content_id}/0

    • 电影
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/movie/{content_id}/0
      http://v3.wufazhuce.com:8000/api/movie/htmlcontent/{content_id}

    • 文章内容和作者
      http://v3.wufazhuce.com:8000/api/essay/htmlcontent/{content_id}
      文章评论
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/essay/{content_id}/0

  6. 搜索模块分类跳转(category_id,0图文 3问答 1阅读 2连载 5影视 4音乐 8电台)

    http://v3.wufazhuce.com:8000/api/all/list/{category_id}

  7. one首页和专题列表的item点击阅读跳转详情和获取评论

    • 问答
      http://v3.wufazhuce.com:8000/api/question/htmlcontent/{content_id}
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/question/{content_id}

    • 连载
      http://v3.wufazhuce.com:8000/api/serialcontent/htmlcontent/{content_id}
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/serial/{content_id}/0

    • 音乐
      http://v3.wufazhuce.com:8000/api/music/htmlcontent/{content_id}
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/music/{content_id}/0

    • 电影
      http://v3.wufazhuce.com:8000/api/movie/htmlcontent/{content_id}
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/movie/{content_id}/0

    • 文章内容和作者
      http://v3.wufazhuce.com:8000/api/essay/htmlcontent/{content_id}

    • 文章评论
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/essay/{content_id}/0

    • 电台
      http://v3.wufazhuce.com:8000/api/radio/htmlcontent/{content_id}
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/radio/{content_id}/0

    • 专题
      http://v3.wufazhuce.com:8000/api/topic/htmlcontent/{content_id}
      http://v3.wufazhuce.com:8000/api/comment/praiseandtime/topic/{content_id}/0

  8. 作者页
    http://v3.wufazhuce.com:8000/api/author/works?page_num={page_num}&author_id={author_id}&version=4.3.4

功能模块:

根据android 4.3.4整理:

一级界面:

欢迎界面,按星期几展示不同的欢迎界面

one分页,也是首页,展示今日列表(插图,one-story, 问答,文章,影视,音乐,电台),折叠菜单,可翻页查看以前的列表,每翻一页,查看前一天的内容,列表头部可以下拉刷新。

all分页,长列表展示,顶部banner,分类导航,专题列表(下拉到底部以后,可加载更多),列表头部可以下拉刷新。

me分页,个人资料页,未登录时,为登录界面入口和设置入口

二级界面:

设置界面

阅读界面

登录界面

分享界面

搜索界面

第三方平台分享,登录sdk对接

当前未完成部分:

阅读界面js回调(包含音乐播放)夜间模式

设置模块缓存

阅读界面交互动画

目前用到的技术:

由于我最初学习的时候资料是用ES5进行编写的,这是我自学ReactNative后的第一个项目,所以本项目ES5和ES6混用,我知道这样不是很好,有时间了会全部使用ES6

react-native对原生组件进行了封装。

  1. 基本控件的使用 View,Text,Image,ScrollView,ListView,WebView,Clipboard,Platform,TouchableOpacity,ActivityIndicator,StatusBar,SliderBar。
  2. 动画的调用,Animated,Easing
  3. 计时器的使用,react-timer-mixin 改为ES6语法,该模块移除
  4. react-native调用原生模块(原生toast,调android第三方分享sdk)
  5. 底部导航栏TabNavigator,页面导航Navigator
  6. 子组件的封装,调用以及回调
  7. 父组件和子组件之间的参数传递
  8. 页面导航跳转时的参数传递,以及回调
  9. 下拉刷新,第三方react-native控件的引入( react-native-pull ),并进行部分修改
  10. 自定义折叠控件(one分页中的菜单)实现折叠动画
  11. 自定义banner控件,实现all分页中的广告栏展示
  12. 自定义加载更多控件,监听ScrollView的滑动,实现加载更多
  13. 自定义阅读界面loading时的帧动画展示
  14. 半透明界面,悬浮窗实现,自定义单选对话框实现
  15. 相册,拍照实现,react-native第三方库接入
  16. 调用基于Android原生封装的ui控件
  17. 在Android原生项目的基础上引入react-native框架进行混合开发
  18. 音乐专辑封面旋转动画(解决动画循环播放中的暂停与播放事件),通过直接修改属性setNativeProps实现帧动画提高性能
  19. 加入mobx实现多界面同时刷新(2018.5.2更新)
  20. 利用高阶组件封装base组件,实现组件的公共逻辑(2018.5.2更新)
  21. 实现one分页标题(日期数字)切换动画 (2018.6.28更新)

这个项目让初学者的我受益良多,现在的react-native第三方框架还不是很多,很多时候要自定义,而且在Android上的坑还是有很多,不过react-native的更新速度也是相当快的。后面有时间会陆续追加一些此项目中自定义部分的详细技术文章。
最近由于公司的项目需求,入坑了微信小程序,年前忙项目进度,估计短期内没有时间更新这个项目了,有时间会上传目前的动画效果图。

此项目的github地址传送门,欢迎一起交流和学习。

RxJava 的异步实现,是通过一种扩展的观察者模式来实现的。免除了业务逻辑代码的频繁回调,增加了逻辑代码的可读性。

观察者模式:

A 对象(观察者)对 B 对象(被观察者)的某种变化高度敏感,需要在 B 变化的一瞬间做出反应。
观察者不需要时刻盯着被观察者(例如 A 不需要每过 2ms 就检查一次 B 的状态),而是采用注册(Register)或者称为订阅(Subscribe)的方式

  • 观察者接口定义
    1
    2
    3
    public interface Observer {
    void update(Object value);
    }
  • 观察者实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class User implements Observer{
    private String name;
    private String message;
    public User(String name){
    this,name = name;
    }
    public void update(Object value){
    this.message = (String) value;
    readMessage();
    }
    private void readMessage(){
    System.out.println(name + "收到推送消息" + message);
    }
    }
  • 被观察者接口定义
    1
    2
    3
    4
    5
    6
    public interface Observable{
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
    void pushMessage(String message);
    }
  • 被观察者实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class WechatServerObservable implements Observable{
    private List<Observer> observers = new ArrayList<>(); // 缓存多个观察者
    private String message;
    public void addObserver(Observer observer){
    observers.add(observer);
    }

    public void removeObserver(Observer observer){
    observers.remove(observer);
    }

    public void notifyObservers(){
    for (Observer observer : observers) {
    observer.update(message);
    }
    }

    public void pushMessage(String message){
    this,message = message;
    notifyObservers();
    System.out.println(name + "发送推送消息" + message);
    }
    }
  • 使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //创建一个被观察者,微信服务
    Observable observable =new WeChatServerObservable();
    //创建多个观察者,用户
    Observer user1 =new UserPerson("小方");
    Observer user2 =new UserPerson("小明");
    observable.addObserver(user1);
    observable.addObserver(user2);
    //推送消息
    observable.pushMessage("开始抢票!");

    RxJava 基本概念:

    Observable (可观察者,即被观察者)也称作上游
    Observer (观察者)也称作下游
    subscribe (订阅)、事件
    Observable 和 Observer 通过 subscribe() 方法实现订阅关系,从而 Observable 可以在需要的时候发出事件来通知 Observer。

与传统观察者模式不同, RxJava 的事件回调方法除了普通事件 onNext() (相当于 onClick() / onEvent())之外,还定义了两个特殊的事件:onCompleted() 和 onError()。

onCompleted(): 事件队列完结。RxJava 不仅把每个事件单独处理,还会把它们看做一个队列。RxJava 规定,当不会再有新的 onNext() 发出时,需要触发 onCompleted() 方法作为标志。
onError(): 事件队列异常。在事件处理过程中出异常时,onError() 会被触发,同时队列自动终止,不允许再有事件发出。
在一个正确运行的事件序列中, onCompleted() 和 onError() 有且只有一个,并且是事件序列中的最后一个。需要注意的是,onCompleted() 和 onError() 二者也是互斥的,即在队列中调用了其中一个,就不应该再调用另一个。

对于Android本身有一套现成的框架,复杂的逻辑操作在服务端较多,客户端出现的场景较少,长时间不怎么使用,很容易让人忘记那些理论知识,所以根据一些实战例子来进行学习效果更佳。

map操作符

常用于数据列表类型转换
上游读取数据源并发送到下游,Utils.getApiUserList()模拟从服务端获得的dto类型为ApiUser的数据列表

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
private Observable<List<ApiUser>> getObservable() {
return Observable.create(new ObservableOnSubscribe<List<ApiUser>>() {
@Override
public void subscribe(ObservableEmitter<List<ApiUser>> e) throws Exception {
//上游未被切断
if (!e.isDisposed()) {
//发送到下游,执行下游对应逻辑
e.onNext(Utils.getApiUserList());
//上游发送完毕,执行下游对应逻辑
e.onComplete();
}
}
});
}


public static List<ApiUser> getApiUserList() {

List<ApiUser> apiUserList = new ArrayList<>();

ApiUser apiUserOne = new ApiUser();
apiUserOne.firstname = "Amit";
apiUserOne.lastname = "Shekhar";
apiUserList.add(apiUserOne);

ApiUser apiUserTwo = new ApiUser();
apiUserTwo.firstname = "Manish";
apiUserTwo.lastname = "Kumar";
apiUserList.add(apiUserTwo);

ApiUser apiUserThree = new ApiUser();
apiUserThree.firstname = "Sumit";
apiUserThree.lastname = "Kumar";
apiUserList.add(apiUserThree);

return apiUserList;
}

建立订阅关系,通过map操作符将ApiUser类型的dto转化成User类型的dto,再执行下游的处理逻辑:

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
    //上游处理逻辑
getObservable()
.map(new Function<List<ApiUser>, List<User>>() {
@Override
public List<User> apply(List<ApiUser> apiUsers) throws Exception {
//转化操作
return Utils.convertApiUserListToUserList(apiUsers);
}
})
// 在子线程中运行
.subscribeOn(Schedulers.io())
// 切换到主线程
.observeOn(AndroidSchedulers.mainThread())
// 下游处理逻辑
.subscribe(getObserver());

public static List<User> convertApiUserListToUserList(List<ApiUser> apiUserList) {

List<User> userList = new ArrayList<>();

for (ApiUser apiUser : apiUserList) {
User user = new User();
user.firstname = apiUser.firstname;
user.lastname = apiUser.lastname;
userList.add(user);
}

return userList;
}

下游获取到数据源,进行数据打印:

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
private Observer<List<User>> getObserver() {
return new Observer<List<User>>() {

@Override
public void onSubscribe(Disposable d) {
Log.d(TAG, " onSubscribe : " + d.isDisposed());
}

@Override
public void onNext(List<User> userList) {
textView.append(" onNext");
textView.append(AppConstant.LINE_SEPARATOR);
for (User user : userList) {
textView.append(" firstname : " + user.firstname);
textView.append(AppConstant.LINE_SEPARATOR);
}
Log.d(TAG, " onNext : " + userList.size());
}

@Override
public void onError(Throwable e) {
textView.append(" onError : " + e.getMessage());
textView.append(AppConstant.LINE_SEPARATOR);
Log.d(TAG, " onError : " + e.getMessage());
}

@Override
public void onComplete() {
textView.append(" onComplete");
textView.append(AppConstant.LINE_SEPARATOR);
Log.d(TAG, " onComplete");
}
};
}

flatMap操作符

常用于遍历数据列表,按条件进行过滤
与它相同的是concatMap,但是flatMap可能会乱序,concatMap可保证下游的获取到的数据源与上游发送顺序一致
此处上游数据源与map操作符相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
getObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Function<List<ApiUser>, ObservableSource<ApiUser>>() {
@Override
public ObservableSource<ApiUser> apply(List<ApiUser> apiUsers) throws Exception {
return Observable.fromIterable(apiUsers);
}
})
.filter(new Predicate<ApiUser>() {
@Override
public boolean test(ApiUser apiUser) throws Exception {
return apiUser.lastname.substring(0,1).equals("K");
}
})
.subscribe(getObserver());

下游获取到数据源,进行数据打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Observer<ApiUser> getObserver(){
return new Observer<ApiUser>() {
@Override
public void onSubscribe(Disposable d) {

}

@Override
public void onNext(ApiUser apiUser) {
Log.d(TAG, "user : " + apiUser.toString());
}

@Override
public void onError(Throwable e) {

}

@Override
public void onComplete() {

}
};
}

zip操作符

常用于对两个数据源列表进行处理,并返回一个结果数据列表
上游读取两个数据源并发送到下游:

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
private Observable<List<User>> getCricketFansObservable() {
return Observable.create(new ObservableOnSubscribe<List<User>>() {
@Override
public void subscribe(ObservableEmitter<List<User>> e) throws Exception {
//上游未被切断
if (!e.isDisposed()) {
//发送到下游,执行下游对应逻辑
e.onNext(Utils.getUserListWhoLovesCricket());
//上游发送完毕,执行下游对应逻辑
e.onComplete();
}
}
});
}

private Observable<List<User>> getFootballFansObservable() {
return Observable.create(new ObservableOnSubscribe<List<User>>() {
@Override
public void subscribe(ObservableEmitter<List<User>> e) throws Exception {
//上游未被切断
if (!e.isDisposed()) {
//发送到下游,执行下游对应逻辑
e.onNext(Utils.getUserListWhoLovesFootball());
//上游发送完毕,执行下游对应逻辑
e.onComplete();
}
}
});
}

//模拟数据源1
public static List<User> getUserListWhoLovesCricket() {

List<User> userList = new ArrayList<>();

User userOne = new User();
userOne.id = 1;
userOne.firstname = "Amit";
userOne.lastname = "Shekhar";
userList.add(userOne);

User userTwo = new User();
userTwo.id = 2;
userTwo.firstname = "Manish";
userTwo.lastname = "Kumar";
userList.add(userTwo);

return userList;
}

//模拟数据源2
public static List<User> getUserListWhoLovesFootball() {

List<User> userList = new ArrayList<>();

User userOne = new User();
userOne.id = 1;
userOne.firstname = "Amit";
userOne.lastname = "Shekhar";
userList.add(userOne);

User userTwo = new User();
userTwo.id = 3;
userTwo.firstname = "Sumit";
userTwo.lastname = "Kumar";
userList.add(userTwo);

return userList;
}

建立订阅关系,通过zip操作符过滤两个数据源都有的数据,返回结果数据列表

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
Observable.zip(getCricketFansObservable(), getFootballFansObservable(),
new BiFunction<List<User>, List<User>, List<User>>() {
@Override
public List<User> apply(List<User> cricketFans, List<User> footballFans) throws Exception {
//过滤两个数据表都有的数据,返回结果数据列表
return Utils.filterUserWhoLovesBoth(cricketFans, footballFans);
}
})
// 在子线程中运行
.subscribeOn(Schedulers.io())
// 切到主线程
.observeOn(AndroidSchedulers.mainThread())
// 下游逻辑处理
.subscribe(getObserver());

public static List<User> filterUserWhoLovesBoth(List<User> cricketFans, List<User> footballFans) {
List<User> userWhoLovesBoth = new ArrayList<User>();
for (User cricketFan : cricketFans) {
for (User footballFan : footballFans) {
if (cricketFan.id == footballFan.id) {
userWhoLovesBoth.add(cricketFan);
}
}
}
return userWhoLovesBoth;
}

下游获取到数据源进行数据打印

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
private Observer<List<User>> getObserver() {
return new Observer<List<User>>() {

@Override
public void onSubscribe(Disposable d) {
Log.d(TAG, " onSubscribe : " + d.isDisposed());
}

@Override
public void onNext(List<User> userList) {
textView.append(" onNext");
textView.append(AppConstant.LINE_SEPARATOR);
for (User user : userList) {
textView.append(" firstname : " + user.firstname);
textView.append(AppConstant.LINE_SEPARATOR);
}
Log.d(TAG, " onNext : " + userList.size());
}

@Override
public void onError(Throwable e) {
textView.append(" onError : " + e.getMessage());
textView.append(AppConstant.LINE_SEPARATOR);
Log.d(TAG, " onError : " + e.getMessage());
}

@Override
public void onComplete() {
textView.append(" onComplete");
textView.append(AppConstant.LINE_SEPARATOR);
Log.d(TAG, " onComplete");
}
};
}

Interval操作符

常用于周期性的处理逻辑(每隔多少时间后执行)
上游11秒延迟后,每隔2秒,发送long型数据源到下游,从0开始逐渐递增

1
2
3
private Observable<? extends Long> getObservable() {
return Observable.interval(11, 2, TimeUnit.SECONDS);
}

建立订阅关系,CompositeDisposable统一存放水管,也就是被观察者,为防止内存泄漏,在activity被destory时切断所有的水管

1
2
3
4
5
6
7
8
9
10
11
12
13
CompositeDisposable disposables = new CompositeDisposable();
disposables.add(getObservable()
// 上游运行在子线程
.subscribeOn(Schedulers.io())
// 切换到主线程
.observeOn(AndroidSchedulers.mainThread())
// 下游逻辑处理
.subscribeWith(getObserver()));
@Override
protected void onDestroy() {
super.onDestroy();
disposables.clear();
}

下游获取到数据源进行数据打印:

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 DisposableObserver<Long> getObserver() {
return new DisposableObserver<Long>() {

@Override
public void onNext(Long value) {
textView.append(" onNext : value : " + value);
textView.append(AppConstant.LINE_SEPARATOR);
Log.d(TAG, " onNext : value : " + value);
}

@Override
public void onError(Throwable e) {
textView.append(" onError : " + e.getMessage());
textView.append(AppConstant.LINE_SEPARATOR);
Log.d(TAG, " onError : " + e.getMessage());
}

@Override
public void onComplete() {
textView.append(" onComplete");
textView.append(AppConstant.LINE_SEPARATOR);
Log.d(TAG, " onComplete");
}
};
}

Timer操作符

常用于计时器的处理逻辑(多少时间后执行)
上游延迟2秒发送long型数据0到下游,只发送一次

1
2
3
private Observable<? extends Long> getObservable() {
return Observable.timer(2, TimeUnit.SECONDS);
}

建立订阅关系

1
2
3
4
5
6
getObservable()
// 上游在子线程运行
.subscribeOn(Schedulers.io())
// 切换到主线程
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getObserver());

下游进行打印输出

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
private Observer<Long> getObserver() {
return new Observer<Long>() {

@Override
public void onSubscribe(Disposable d) {
Log.d(TAG, " onSubscribe : " + d.isDisposed());
}

@Override
public void onNext(Long value) {
textView.append(" onNext : value : " + value);
textView.append(AppConstant.LINE_SEPARATOR);
Log.d(TAG, " onNext : value : " + value);
}

@Override
public void onError(Throwable e) {
textView.append(" onError : " + e.getMessage());
textView.append(AppConstant.LINE_SEPARATOR);
Log.d(TAG, " onError : " + e.getMessage());
}

@Override
public void onComplete() {
textView.append(" onComplete");
textView.append(AppConstant.LINE_SEPARATOR);
Log.d(TAG, " onComplete");
}
};
}

Flowable背压式

Rxjava在处理异步操作时,会先将上游的数据放入一个缓存池中,这里称作水缸,一旦下游不能及时处理就会出现内存溢出的问题,抛出MissingBackpressureException
解决方式

  1. 放慢上游的发送速度
  2. 减少发送的数据
    Flowable在设计的时候采用了一种新的思路也就是响应式拉取的方式来更好的解决上下游流速不均衡的问题,
    在同一线程中下游没有调用request, 上游就认为下游没有处理事件的能力,抛出MissingBackpressureException,当上下游工作在不同的线程中时, 只有当下游调用request时, 才从水缸里取出事件发给下游。

    背压策略

    BackpressureStrategy.ERROR:

    默认策略,有一个大小为128的水缸

    BackpressureStrategy.BUFFER

    增大水缸的容量,仍有内存溢出问题

    BackpressureStrategy.DROP

    水缸存不下时,直接把存不下的事件丢弃

    BackpressureStrategy.LATEST

    水缸存不下时,只保留最新的事件

    reduce操作符

    遍历list,第一次将第一个元素和第二个元素作为入参,返回一个结果值,以后将上一次返回的结果和当前遍历的元素作为入参,返回一个结果值。
    与它相同的是scan操作符,但是scan操作符会返回上一次的结果
    上游创建数据源,发送到下游
    1
    2
    3
    private Observable<Integer> getObservable() {
    return Observable.just(1, 2, 3, 4);
    }
    建立订阅关系,将上一次返回的结果和当前遍历的元素作为入参,最后返回一个累加值
    1
    2
    3
    4
    5
    6
    7
    8
    getObservable()
    .reduce(new BiFunction<Integer, Integer, Integer>() {
    @Override
    public Integer apply(Integer t1, Integer t2) {
    return t1 + t2;
    }
    })
    .subscribe(getObserver());
    下游进行打印输出
    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
    private MaybeObserver<Integer> getObserver() {
    return new MaybeObserver<Integer>() {
    @Override
    public void onSubscribe(Disposable d) {
    Log.d(TAG, " onSubscribe : " + d.isDisposed());
    }

    @Override
    public void onSuccess(Integer value) {
    textView.append(" onSuccess : value : " + value);
    textView.append(AppConstant.LINE_SEPARATOR);
    Log.d(TAG, " onSuccess : value : " + value);
    }

    @Override
    public void onError(Throwable e) {
    textView.append(" onError : " + e.getMessage());
    textView.append(AppConstant.LINE_SEPARATOR);
    Log.d(TAG, " onError : " + e.getMessage());
    }

    @Override
    public void onComplete() {
    textView.append(" onComplete");
    textView.append(AppConstant.LINE_SEPARATOR);
    Log.d(TAG, " onComplete");
    }
    };
    }

    Filter操作符

    遍历list中的每个元素,按条件过滤
    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
    Observable.just(1, 2, 3, 4, 5, 6)
    .filter(new Predicate<Integer>() {
    @Override
    public boolean test(Integer integer) throws Exception {
    return integer % 2 == 0;
    }
    })
    .subscribe(getObserver());


    private Observer<Integer> getObserver() {
    return new Observer<Integer>() {

    @Override
    public void onSubscribe(Disposable d) {
    Log.d(TAG, " onSubscribe : " + d.isDisposed());
    }

    @Override
    public void onNext(Integer value) {
    textView.append(" onNext : ");
    textView.append(AppConstant.LINE_SEPARATOR);
    textView.append(" value : " + value);
    textView.append(AppConstant.LINE_SEPARATOR);
    Log.d(TAG, " onNext ");
    Log.d(TAG, " value : " + value);
    }

    @Override
    public void onError(Throwable e) {
    textView.append(" onError : " + e.getMessage());
    textView.append(AppConstant.LINE_SEPARATOR);
    Log.d(TAG, " onError : " + e.getMessage());
    }

    @Override
    public void onComplete() {
    textView.append(" onComplete");
    textView.append(AppConstant.LINE_SEPARATOR);
    Log.d(TAG, " onComplete");
    }
    };
    }

    Contact操作符

    将多个数据源结合成一个数据源并发送数据,并且严格按照先后顺序发射数据
    与它相同的有merge,但是merge是无序的且是并发的
    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
    String[] aStrings = {"A1", "A2", "A3", "A4"};
    String[] bStrings = {"B1", "B2", "B3"};

    Observable<String> aObservable = Observable.fromArray(aStrings);
    Observable<String> bObservable = Observable.fromArray(bStrings);

    Observable.concat(aObservable, bObservable)
    .subscribe(getObserver());

    private Observer<String> getObserver() {
    return new Observer<String>() {

    @Override
    public void onSubscribe(Disposable d) {
    Log.d(TAG, " onSubscribe : " + d.isDisposed());
    }

    @Override
    public void onNext(String value) {
    textView.append(" onNext : value : " + value);
    textView.append(AppConstant.LINE_SEPARATOR);
    Log.d(TAG, " onNext : value : " + value);
    }

    @Override
    public void onError(Throwable e) {
    textView.append(" onError : " + e.getMessage());
    textView.append(AppConstant.LINE_SEPARATOR);
    Log.d(TAG, " onError : " + e.getMessage());
    }

    @Override
    public void onComplete() {
    textView.append(" onComplete");
    textView.append(AppConstant.LINE_SEPARATOR);
    Log.d(TAG, " onComplete");
    }
    };
    }

    Rxbus的使用

    定义Rxbus
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class RxBus {

    public RxBus() {
    }

    private PublishSubject<Object> bus = PublishSubject.create();

    //发送event
    public void send(Object o) {
    bus.onNext(o);
    }

    //获取bus对象
    public Observable<Object> toObservable() {
    return bus;
    }

    public boolean hasObservers() {
    return bus.hasObservers();
    }
    }
    在Application中初始化Rxbus,做事件的统一管理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class MyApplication extends Application {
    private RxBus bus;
    @Override
    public void onCreate() {
    super.onCreate();
    bus = new RxBus();
    }

    public RxBus bus() {
    return bus;
    }
    }
    在activity中发送事件,CompositeDisposable统一存放水管,为避免内存泄漏,destory时切断水管
    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
    CompositeDisposable disposables = new CompositeDisposable();
    @Override
    protected void onDestroy() {
    super.onDestroy();
    disposables.clear();
    }
    //上游是MyApplication的bus,建立订阅关系,绑定下游事件接收回调处理
    disposables.add(((MyApplication) getApplication())
    .bus()
    .toObservable()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Consumer<Object>() {
    @Override
    public void accept(Object object) throws Exception {
    if (object instanceof Events.TapEvent) {
    textView.setText("Tap Event Received");
    }
    }
    }));

    //发送事件到MyApplication的bus
    ((MyApplication) getApplication())
    .bus()
    .send(new Events.TapEvent());

    操作符组合使用

    android搜索功能的实现
    绑定searchview的监听
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class RxSearchObservable {
    public static Observable<String> fromView(SearchView searchView) {

    final PublishSubject<String> subject = PublishSubject.create();

    searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
    @Override
    public boolean onQueryTextSubmit(String s) {
    subject.onComplete();
    return true;
    }

    @Override
    public boolean onQueryTextChange(String text) {
    subject.onNext(text);
    return true;
    }
    });

    return subject;
    }
    }
    建立订阅关系,上游输入文字监听,下游显示结果
    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
    //上游绑定SearchView输入文字监听
    RxSearchObservable.fromView(searchView)
    //每隔300毫秒执行
    .debounce(300, TimeUnit.MILLISECONDS)
    //过滤输入文字不为空
    .filter(new Predicate<String>() {
    @Override
    public boolean test(String text) throws Exception {
    if (text.isEmpty()) {
    textViewResult.setText("");
    return false;
    } else {
    return true;
    }
    }
    })
    //与上次的数据对比有变化
    .distinctUntilChanged()
    //只发射最近的一次observable
    .switchMap(new Function<String, ObservableSource<String>>() {
    @Override
    public ObservableSource<String> apply(String query) throws Exception {
    //从网络获取数据
    return dataFromNetwork(query);
    }
    })
    //在子线程中运行
    .subscribeOn(Schedulers.io())
    //切到主线程
    .observeOn(AndroidSchedulers.mainThread())
    //下游显示结果
    .subscribe(new Consumer<String>() {
    @Override
    public void accept(String result) throws Exception {
    textViewResult.setText(result);
    }
    });

    //模拟网络请求
    private Observable<String> dataFromNetwork(final String query) {
    return Observable.just(true)
    .delay(2, TimeUnit.SECONDS)
    .map(new Function<Boolean, String>() {
    @Override
    public String apply(@NonNull Boolean value) throws Exception {
    return query;
    }
    });
    }

    封装线程切换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    private final static <UD> ObservableTransformer <UD, UD> rxud(){
    return new ObservableTransformer<UD,UD>(){
    public ObservableSource<UD> apply (Observable<UD> upstream){
    return upstream.subscribeOn(Schedulers,io())
    .observableOn(AndroidSchedulers.mainThread());
    }
    }
    }

    Observable.from(getObservable())
    .map(data -> dealwith(data))
    .compose(rxud())
    .subscribe(getObserver());

    切子线程切主线程再切子线程切主线程

    doOnNext 替代subscribeOn连接两次请求之间的切换
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    api.registerAction()
    .subscribeOn(Schedulers.io())
    .observableOn(AndroidSchedulers.mainThread())
    .doOnNext(new Consumer<RegisterResponse>(){
    public void accept(RegisterResponse registerResponse) throws Exception {

    }
    })
    .observableOn(Schedulers.io())
    .flatMap(new Function<RegisterResponse, ObservableSource<LoginResponse>>(){
    public ObservableSource<LoginResponse> apply(RegisterResponse registerResponse){
    Observable<LoginResponse> loginResponse = api.loginAction();
    return loginResponse;
    }
    })
    .observableOn(AndroidSchedulers.mainThread())
    .subscribe(new Observable<LoginResponse>{

    })

    封装RXView

    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
    public class RxView{
    private final static String TAG = RxView.class.getSimpleName();
    public static Observable<Object> clicks(View view){
    return new ViewClickObservable(view);
    }

    }

    public class ViewClickObservable extends Observable<Object> {

    private final View view;
    private static final Object EVENT = new Object();
    private static Object EVENT2;
    public ViewClickObservable(View view){
    this.view = view;
    EVENT2 = view;
    }
    protected void subscribeActual(Observer<? super Object> observer){
    MyListener myListener = new MyListener(view, observer);
    observer.onSubscribe(myListener)

    this.view.setOnClickListener(myListener)
    }

    static final class MyListener implements View.OnClickListener, Disposable{
    private final View view;
    private Observer<Object> observer;
    private final AtomicBoolean isDisposable = new AtomicBoolean(); // 是否中断

    public MyListener(View view, Observer<Object> observer){
    this.view = view;
    this.observer = observer;
    }
    public void onClick(View v){
    // 继续分发
    if (isDisposed() == false){
    observer.onNext(EVENT);
    }
    }

    public void dispose(){
    // 之前没有中断,设置为中断
    if (isDisposable.compareAndSet(false, true)){
    // 主线程取消监听逻辑
    if (Looper.myLoop() == Looper.getMainLooper()){
    view.setOnClickListener(null)
    }else {
    AndroidSchedulers.mainThread().schedulerDirect(new Runnable()){
    public void run(){
    view.setOnClickListener(null)
    }
    }
    }
    }
    }

    public boolean isDisposed(){
    return isDisposable;
    }
    }
    }

    RxView.clicks(button)
    .throttleFirst(2000, TimeUnit.MILLISECONDS)
    .subscribe(new Consumer<Object>(){
    public void accept(Object o) throws Exception {
    Observable.create(new ObservableOnSubscribe<String>(){
    public void subscribe(ObservableEmitter<String> e) throws Exception{
    e.onNext("click");
    }
    })
    }
    })
    .subscribe(new Consumer<String>(){
    public void accept(String s) throws Exception {
    Log.d("RxView","accept:" + s)
    }
    })

java9已经发布一段时间了,AndroidStudio才开始支持Java 8,Java 8是Java的一个重大版本,虽然这些新特性令Java开发人员十分期待,但同时也需要花不少精力去学习,对于android开发者而言了解Java8的新特性是必要的,虽然现在Google力推kotlin,但是Java仍然是排名第一的使用最广泛的语言,社区依旧很活跃,也是处于不断进步和发展中,而且在新特性中我们能看到一些kotlin的影子。

lambda表达式的支持

格式为:(入参) -> {函数体}

基本形式:

入参和返回值的类型可被编译器推测,入参一个参数不需要括号,无参直接写为(),函数体多行可加上括号

1
2
3
(int a, int b) -> a + b;
(a, b) -> a - b;
(int a, int b) -> { return a * b; };

用法:

for循环

以前的写法:

1
2
3
for (String player : players) {  
System.out.print(player + "; ");
}

lambda写法:

1
players.forEach((player) -> System.out.print(player + "; "));

ui回调

以前的写法:

1
2
3
4
5
6
check_all.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

}
});

lambda写法:

1
check_all.setOnClickListener(v->{});

创建线程

以前的写法:

1
2
3
4
5
6
new Thread(new Runnable() {  
@Override
public void run() {
System.out.println("Hello world !");
}
}).start();

lambda写法:

1
new Thread(() -> System.out.println("Hello world !")).start();

排序

以前的写法:

1
2
3
4
5
6
Arrays.sort(players, new Comparator<String>() {  
@Override
public int compare(String s1, String s2) {
return (s1.length() - s2.length());
}
});

lambda写法:

1
Arrays.sort(players, (String s1, String s2) -> (s1.length() - s2.length()));

结合Stream

Stream是对集合的包装,通常和lambda一起使用。 使用lambdas可以支持许多操作,如 map, filter, limit, sorted, count, min, max, sum, collect 等等。
定义一个类,医生

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
public class Doctor {
private String name;
private int age;
private String degree;
private int salary;

public Doctor(String name, int age, String degree, int salary) {
this.name = name;
this.age = age;
this.degree = degree;
this.salary = salary;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}


public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getDegree() {
return degree;
}

public void setDegree(String degree) {
this.degree = degree;
}

public int getSalary() {
return salary;
}

public void setSalary(int salary) {
this.salary = salary;
}
}

先初始化医生列表,foreach循环输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<Doctor> doctors =new ArrayList<>();
doctors.add(new Doctor("joe",20,"bachelor",2000));
doctors.add(new Doctor("jack",23,"bachelor",2300));
doctors.add(new Doctor("kris",27,"Master",4200));
doctors.add(new Doctor("bob",28,"doctor",5000));
doctors.add(new Doctor("tom",26,"Master",4400));
doctors.add(new Doctor("jay",29,"doctor",5500));

doctors.forEach(
(doctor)->{
System.out.println(doctor.getName());

}
);

过滤器filter(),过滤出年龄大于25岁且学历为博士的医生

1
2
3
doctors.stream().filter( doctor -> doctor.getAge()>25)
.filter(doctor -> "doctor".equals(doctor.getDegree()))
.forEach(doctor -> System.out.println(doctor.getName()));

limit方法,限制结果个数,年龄大于25的前三个医生

1
2
3
doctors.stream().filter( doctor -> doctor.getAge()>25)
.limit(3)
.forEach(doctor -> System.out.println(doctor.getName()));

sorted方法,按薪水由多到少排序

1
2
3
4
5
6
doctors.stream().sorted((d1,d2)->d2.getSalary()-d1.getSalary())
.forEach(
(doctor)->{
System.out.println(doctor.getName());
}
);

min和max方法,找出最低和最高的薪水

1
2
3
4
5
Doctor doctor1=doctors.stream().min((d1,d2)->d1.getSalary()-d2.getSalary()).get();
System.out.print(doctor1.getName());

Doctor doctor2=doctors.stream().min((d1,d2)->d1.getSalary()-d2.getSalary()).get();
System.out.print(doctor2.getName());

结合 map 方法,使用 collect 方法来将我们的结果集放到一个字符串,一个 Set 中
将所有医生的名字拼接在一个string中,用逗号隔开

1
2
String names=doctors.stream().map(Doctor::getName).collect(joining(","));
System.out.print(names);

将所有医生的名字存放到一个set中

1
2
Set<String> doctorSet=doctors.stream().map(Doctor::getName).collect(Collectors.toSet());
octorSet.forEach((name)->System.out.println(name));

summaryStatistics方法获得stream中元素的汇总数据,求和,平均值,最大最小值,个数

1
2
3
4
5
6
7
8
IntSummaryStatistics statistics=doctors.stream()
.mapToInt(doctor->doctor.getSalary())
.summaryStatistics();
System.out.println(statistics.getSum());
System.out.println(statistics.getAverage());
System.out.println(statistics.getMax());
System.out.println(statistics.getMin());
System.out.println(statistics.getCount());

接口的默认方法和静态方法

默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法可以被接口的实现类继承或者覆写。

1
2
3
4
5
6
7
8
9
public interface MyInterface{
default String defaultMethod(){
return "this is default method";
}
}

public class MyClass implements MyInterface{

}

在接口中可以定义静态方法

1
2
3
4
5
6
7
8
public interface MyInterface{
default String defaultMethod(){
return "this is default method";
}
static String staticMethod(){
return "this is static method";
}
}

方法的引用方式

初始化数组

1
2
3
4
5
6
Function<Integer, String[]> fun = x -> new String[x];
String[] strs = fun.apply(10);
System.out.println(strs.length);
Function<Integer,String[]> fun1 = String[]::new;
strs = fun1.apply(20);
System.out.println(strs.length);

构造方法的引用 Class::new只针对无参构造,若该类只有有参构造,需要添加一个无参构造方法:

1
2
3
4
Supplier<Student> supplier=Student::new;
Student student=supplier.get();
student.setName("小明");
System.out.println(student.getName());

有参构造调用

1
2
3
Supplier<Student> supplier=()->new Student("小张","man",12);
Student student=supplier.get();
System.out.println(student.getName());

静态方法调用

1
2
Supplier<String> supplier=Student::saying;
System.out.println(supplier.get());

成员方法调用

1
2
3
4
5
6
7
Supplier<Student> supplier=Student::new;
Student student=supplier.get();
Consumer<String> consumer1=student::setName;
consumer1.accept("小明");
Consumer<Integer> consumer2=student::setAge;
consumer2.accept(18);
System.out.println(student.getName()+":"+student.getAge());

注解的变化

支持重复注解

1
2
3
4
@Filter( "filter1" )
@Filter( "filter2" )
public interface Filterable {
}

调用

1
Filterable.class.getAnnoation(Filters.class)

控制台输出:
filter1
filter2

注解使用范围扩大

java8可以使用在任何元素上:局部变量、方法、接口、类和泛型,甚至可以用在函数的异常定义上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Retention( RetentionPolicy.RUNTIME )
@Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
public @interface NonEmpty {
}

public static class Holder< @NonEmpty T > extends @NonEmpty Object {
public void method() throws @NonEmpty Exception {
}
}

@SuppressWarnings( "unused" )
public static void main(String[] args) {
final Holder< String > holder = new @NonEmpty Holder< String >();
@NonEmpty Collection< @NonEmpty String > strings = new ArrayList<>();
}

Map新增方法

初始化一个map

1
2
3
4
Map<Integer, String> map = new HashMap<>();
map.put(1, "Tom");
map.put(2, "Bob");
map.put(3, "Jack");

forEach 遍历

1
map.forEach((key,value)->System.out.println(key+":"+value));

getOrDefault

如果指定的key存在,则返回该key对应的value,如果不存在,则返回指定的值。

1
System.out.println(map.getOrDefault(4, "name"));

replaceAll修改所有的value值

1
2
3
//所有value前面加上一个编号
map.replaceAll((key,value)->"20000"+key+value);
map.forEach((key,value)->System.out.println(key+":"+value));

putIfAbsent返回key对应的value,如果value为空则填入一个值

1
2
3
4
5
6
map.putIfAbsent(3, "xiaoming");
map.putIfAbsent(4, "xiaoming");
//Jack
System.out.println(map.get(3));
//xiaoming
System.out.println(map.get(4));

remove指定元素删除

如果key获取的value值与给的value值相等,则删除这个元素

1
2
3
4
5
6
7
map.remove(1, "Bob");
// 未删除成功, 输出 Tom
System.out.println(map.get(1));

map.remove(2, "Bob");
// 删除成功,输出 null
System.out.println(map.get(2))

replace替换

如果key获取的value值与给的value值相等,则把这个value替换成一个新的value

1
2
3
4
5
6
map.replace(3, "Tom", "Joe");
// 未替换成功,输出 Jack
System.out.println(map.get(3));
map.replace(1, "Tom", "Joe");
// 替换成功, 输出 Joe
System.out.println(map.get(1));

如果map中存在key,则替换成value值,替换成功返回旧值,否则返回null

1
2
3
4
5
6
7
8
//输出Tom
System.out.println( map.replace(1, "Joe"));
//输出Joe
System.out.println( map.get(1));
//输出null
System.out.println( map.replace(4, "Joe"));
//输出null
System.out.println( map.get(4));

computeIfAbsent

如果指定的key不存在,则可通过key计算value为新的值

1
2
3
4
5
6
map.computeIfAbsent(1,key->key+"newName");
//输出Tom
System.out.println(map.get(1));
map.computeIfAbsent(4,key->key+"newName");
//输出4newName
System.out.println(map.get(4));

computeIfPresent

如果指定的key存在,则根据key和value计算一个新的newValue, 如果这个newValue不为null,则设置key新的值为这个newValue, 如果newValue为null, 则删除该key的值

1
2
3
4
5
6
7
map.computeIfPresent(1, (key, value) -> key + "新的值:" + value);
// 输出1新的值:Tom
System.out.println(map.get(1));

map.computeIfPresent(2, (key, value) -> null);
// 输出 null
System.out.println(map.get(2));

compute

如果指定的key不存在,则设置指定的value值,否则根据key和value计算一个新的newValue, 如果这个newValue不为null,则设置key新的值为这个newValue, 如果newValue为null, 则删除该key的值。
这个解释有点长,容易绕,实际上用if-else可以很简单的表达

1
2
3
4
5
6
if(!key.exist()){
map.computeIfAbsent(key,key->key+"newValue");
}
else{
map.computeIfPresent(key, (key, value) -> key + "新的值:" + value);
}

所以就是if key不存在,调computeIfAbsent,否则调computeIfPresent

1
2
3
4
5
6
map.compute(4,(key,value)->key+":"+value+"新的值");
//输出 4:null新的值
System.out.println(map.get(4));
map.compute(1,(key,value)->key+":"+"新的值");
//输出 1:新的值
System.out.println(map.get(1));

merge

如果指定的key不存在,直接设置指定值,如果存在根据旧的value,指定值计算出新的值newValue, 如果这个newValue不为空,设置key的新值newValue,如果newValue为null, 则删除该key。最后返回当前最新的值

1
2
3
4
5
6
  //输出 指定的名字
System.out.println(map.merge(4,"指定的名字",(oldValue,assignValue)->oldValue+","+assignValue));
//输出 Tom,指定的名字
System.out.println(map.merge(1,"指定的名字",(oldValue,assignValue)->oldValue+","+assignValue));
//输出 null
System.out.println(map.merge(1,"指定的名字",(oldValue,assignValue)->null));

空指针判断类Optional

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String str1=null;
String str2="abc";
//创建初始化Optional对象,可为空的
Optional<String> optionalStr1=Optional.ofNullable(str1);
//创建初始化Optional对象,不可为空,会抛异常
Optional<String> optionalStr2=Optional.of(str2);
System.out.println("第一个参数是否存在"+optionalStr1.isPresent());
System.out.println("第二个参数是否存在"+optionalStr2.isPresent());
//不存在时返回默认值,存在时返回该值
String getStr1=optionalStr1.orElse("这个值不存在的默认值");
//返回该值,该值必须存在
String getStr2=optionalStr2.get();
System.out.println(getStr1);
System.out.println(getStr2);

日期和时间Joda-Time的支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取当前的日期时间
LocalDateTime currentTime = LocalDateTime.now();
System.out.println("当前时间: " + currentTime);
// 获取当前日期
LocalDate currentDate = currentTime.toLocalDate();
System.out.println("当前日期: " + currentDate);
System.out.println("年"+currentDate.getYear()+"月"+currentDate.getMonthOfYear()+"日"+currentDate.getDayOfMonth());
// 修改当前时间的月份和年份
currentDate.withYear(2013).withMonthOfYear(3);
System.out.println("修改后的日期: " + currentDate);
// 日期格式化
System.out.println(currentDate.toString("yyyy-MM-dd"));
// 修改时区
DateTimeZone zoneEurope = DateTimeZone.forID("Europe/London");
DateTime dtLondon = new DateTime().withZone(zoneEurope);
System.out.println("修改时区: " + dtLondon);
DateTimeZone currentZone = DateTimeZone.getDefault();
DateTime dtCurrent = new DateTime().withZone(currentZone);
System.out.println("当期时区: " + dtCurrent);

base64支持

基本编解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
try {
// 使用基本编码
String base64encodedString = Base64.getEncoder().encodeToString("这是待编码的字符串".getBytes("utf-8"));
System.out.println("Base64编码后" + base64encodedString);
// 解码
byte[] base64decodedBytes = Base64.getDecoder().decode(base64encodedString);
System.out.println("原始字符串: " + new String(base64decodedBytes, "utf-8"));
//使用url编码
String base64encodedUrl = Base64.getUrlEncoder().encodeToString("这是待编码的字符串".getBytes("utf-8"));
System.out.println("Base64 URL编码:" + base64encodedUrl);
//生成随机uuid
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 1; ++i) {
stringBuilder.append(UUID.randomUUID().toString());
}
//使用mime编码
byte[] mimeBytes = stringBuilder.toString().getBytes("utf-8");
String mimeEncodedString = Base64.getMimeEncoder().encodeToString(mimeBytes);
System.out.println("Base64 MIME编码:" + mimeEncodedString);
}catch(UnsupportedEncodingException e){
System.out.println("Error :" + e.getMessage());
}

本人是比较喜欢用模拟器调试的,毕竟可以不用对真机进行频繁的插拔,避免USB接触不良的问题,提高开发效率,虽然模拟器测试时仍有很多限制,但是能用模拟器调试的时候还是会尽量选择模拟器。genymotion是一款优秀的android模拟器,依赖于virtual box,是android开发者的必备品。虽然android studio自带的模拟器已经比以前好多了,但是仍然不是很稳定,莫名重启的情况频频发生,所以genymotion依然是最好的选择。但是目前的genymotion版本都是基于x86架构的,你是否还在为它安装不上输入法和微信而烦恼?那是因为市面上的绝大多数手机都是基于arm架构的,大多数的so库也是仅支持ARM架构,要想让ARM架构的应用在genymotion上跑起来,需要刷ARM_Translation架构包来达到兼容的效果。

genymotion有一个坑就是不同版本的android系统需要刷对应版本的ARM_Translation架构包,否则即使提示刷入成功,仍然是没有效果,应用依旧安装失败。下面是我收集的各个版本的ARM_Translation架构包,亲测可用:

https://pan.baidu.com/s/1eSyBfea

里面有3个文件:

Genymotion_ARM_Translation_Marshmallow.zip 适配android6.0和7.0

Genymotion_ARM_Translation_5.1_Lollipop 适配android5.1

Genymotion-ARM-Translation_v1.1.zip 适配android5.0以下

安装方法很简单,直接把zip包拖到模拟器即可。


随着近几年web前端的火热,纯原生的开发需求逐渐减少,混合开发也变得越来越流行。连最近刷论坛也传着不会前端的android开发不是好的开发者这么一句话。上一个项目快要结束,最近也闲下来了,带着对前端的好奇和提升自身技术的想法,入了混合开发的坑。react-native是Facebook打造的一款开源的跨平台移动应用开发框架,作为最火的跨平台移动应用开发开发框架,官网上写着Learn once, write anywhere,吸引了大批开发者。它弥补了web app性能上的缺陷,调用原生组件,利用javascript开发,支持热更新,跨平台的特性也降低了开发成本。但是目前存在少量bug,我在入门学习的时候也遇到了不少坑,下面做简要记录。


xcode编译时报错

1.’boost/xxx/xxx.hpp’ file not found

由于 /Users/Vanessa/.rncache 中 boost_1_63_0.tar.gz, double-conversion-1.1.5.tar.gz, folly-2016.09.26.00.tar.gz, glog-0.3.4.tar.gz 文件下载不完整

导致 node_modules/react-native/third-party 文件不完整

解决方案:

删除 .rncache 后手动下载一份,后放入 .rncache 中

把以上文件解压后放入 node_modules/react-native/third-party 下

Clean & Build

2.”config.h” file not found
解决方案:
项目目录下执行npm install

之前在网上查到要在package.json中改react-native的版本号,我这边改了反倒报错

android studio报错

编译报错

1.unable to load script form assets

解决方案:

在 android/app/src/main 目录下创建一个 assets空文件夹

在跟目录下执行

react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/

点击菜单刷新报错

红屏显示Could not connect to development server

解决方案:

react-native start

在公司的windows上配置环境,android平台始终报这个错,各种方案都试过了

最后的解决方案是:

在android设备的菜单中选择最后一个选项,配置编译机器的ip地址和端口号8081。

注意:6.0以上的模拟器不要忘记连上wifi

###记2017.12.20爬坑

当你的app有多张图片时,如果你发现在android机上某些图片显示不出,不管是嵌套在需要loadmore的listview,还是性能更高的升级版flatlist以及scrollview,这种类似图片长列表加载到固定页数,后面偶尔只能显示出一两张这种问题,是由于内存问题导致的,最简单的解决办法,分配更大的堆内存,在Android的AndroidManifest配置文件中开启largeHeap,在application标签下添加android:largeHeap="true"即可,整个过程没有任何警告和错误,真的是太坑了。

记2018.1.18爬坑

react-native Android真机调试技巧:

目前网上这块的资料比较少,基本都是如官网所说摇一摇或者是通过adb命令发送一个双击R的输入事件

然而这并不实用,真机调试的时候经常都是连着数据线,每次reload都要摇一摇触发难度系数比较高,输入adb命令显得过于麻烦。实际上我们使用reactnative绘制的activity都是继承了ReactActivity,查看一下源码,它重写了按键监听,我们可以看到这样的代码

1
2
3
4
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
}

我们点击进去mDelegate.onKeyUp(keyCode, event)继续跟踪,可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
if (keyCode == KeyEvent.KEYCODE_MENU) {
getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
return true;
}
boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)
.didDoubleTapR(keyCode, getPlainActivity().getCurrentFocus());
if (didDoubleTapR) {
getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
return true;
}
}
return false;
}

所以我们点菜单键调用的是getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();弹出调试菜单。

我们双击R键,调用的是getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();进行reload刷新。

这两个方法在ReactActivity直接调用,纯react-native搭建的项目,我们可以在onCreate时调用方便调试。

如果是在混合开发的项目,onCreate方法中我们有自己的ReactInstanceManager,可以直接调用 mReactInstanceManager.getDevSupportManager().handleReloadJS();菜单弹出也一样。

记2018.8.28爬坑

当前最新的react-native版本0.56.0引入mobx报错
网上诸多教程为老版本引入方式,过程如下:
npm安装以下插件

1
npm i babel-plugin-transform-decorators-legacy babel-preset-react-native-stage-0 --save-dev

修改 .babelrc 文件配置 babel 插件

1
2
3
4
{
'presets': ['react-native'],
'plugins': ['transform-decorators-legacy']
}

这个方式已经过时了,你会得到以下错误,看起来是babel的问题:

1
error: bundling failed: TypeError: Property right of AssignmentExpression expected node to be of a type ["Expression"] but instead got null

google无数次后在github上issue查到babel@7.0+不能再使用原先的babel-plugin-transform-decorators-legacy,这个只能在babel@6以下使用,而新版本rn使用正好使用的是babel@7.0+
正确的.babelrc如下:

1
2
3
4
5
6
{
"presets": ["react-native"],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }]
]
}

注意,这里的plugin-proposal-decorators的版本必须与babel的版本完全一样,
在yarn.lock文件中可以查看babel的版本如下:

1
2
3
4
5
6
7
8
9
10
"@babel/core@7.0.0-beta.47":
version "7.0.0-beta.47"
resolved "http://r.cnpmjs.org/@babel/core/download/@babel/core-7.0.0-beta.47.tgz#b9c164fb9a1e1083f067c236a9da1d7a7d759271"
dependencies:
"@babel/code-frame" "7.0.0-beta.47"
"@babel/generator" "7.0.0-beta.47"
"@babel/helpers" "7.0.0-beta.47"
"@babel/template" "7.0.0-beta.47"
"@babel/traverse" "7.0.0-beta.47"
"@babel/types" "7.0.0-beta.47"

如果你的版本是@7.0.0-beta.47那么用如下命令安装
yarn add @babel/plugin-proposal-decorators@7.0.0-beta.47


随着Android技术发展越来越成熟,随着热更新技术的火热,组件化和插件化的技术也得到了一定的关注。特别是对于比较大的项目,人员的增多,代码越来越臃肿,这时候就必须进行模块化的拆分。模块化在Android工程中目前有两种实现方式,一种是组件化,另一种是插件化,其实质都是降低耦合。作为Android开发者,有必要对这两种技术做一个了解,在项目需要重构时或在构建大项目初期时,选择合适的模块化方案,下面就对组件化技术学习做一个总结。目前比较流行的组件化方案是阿里的Atlas,得到app的AndroidComponent,插件化比较流行的方案是滴滴的virtual apk,360的replugin。


得到团队的AndroidComponent的学习成本较低,对于初学者方便入手,以下记录该框架的学习情况:
每个组件都可以看成一个单独的整体,可以按需的和其他组件(包括主项目)整合在一起,从而完成的形成一个app,整体app缺少任何一个组件都是可以正常运行的,并且每个组件可以单独运行。
件化和插件化的最大区别(应该也是唯一区别)就是组件化在运行时不具备动态添加和修改组件的功能,但是插件化是可以的。

依赖库与Component的区别

依赖库library

代码被其他组件直接引用。比如网络库module可以认为是一个library。

Component

这种module是一个完整的功能模块。比如分享module就是一个Component。
统一把library称之为依赖库,把Component称之为组件。组件化也主要是针对Component这种类型。主项目、主module或者Host负责拼装这些组件以形成一个完成app的module,统一称为主项目。

项目结构

app是主项目,负责集成众多组件,控制组件的生命周期
xxxComponent 是我们拆分的两个组件,比如shareComponent
componentservice 定义了所有的组件提供的服务
basicres 定义了全局通用的theme和color等公共资源
basiclib 公共的基础库,统一在这里引入,比如一些第三方的库okhttp
componentlib 组件化的基础库 Router/UIRouter等都定义在这里
build-gradle 组件化编译的gradle插件

单独调试和发布

  1. 在组件工程下的gradle.properties文件中设置一个isRunAlone的变量来区分不同的场景,build.gradle需要引入一个插件,插件中会判断apply com.android.library还是com.android.application
  2. 在src/main/runalone下面定义单独调试所必须的AndroidManifest.xml、application、入口activity等类
  3. 组件开发并测试完成时,需要发布一个release版本的aar文件到中央仓库,只需要把isRunAlone修改为false,执行module:assembleRelease命令。只有发布组件时才需要修改isRunAlone=false,即使后面将组件集成到app中,也不需要修改isRunAlone的值。所以在Androidstudio中,是可以看到三个application工程的,随便点击一个都是可以独立运行的,并且可以根据配置引入其他需要依赖的组件,这些工作都由插件来完成。

组件交互

组件间数据传输,通过接口+实现的方式,组件之间完全面向接口编程。
下面是share提供一个fragment给app使用的例子。
share组件在componentservice中定义自己的服务

1
2
3
public interface ShareService {
Fragment getShareFragment();
}

在自己的组件工程中,提供对应的实现类ShareServiceImpl:

1
2
3
4
5
6
public class ShareServiceImpl implements ShareService {
@Override
public Fragment getShareFragment() {
return new ShareFragment();
}
}

在ShareAppLike中在组件加载的时候把实现类注册到Router中,ShareAppLike相当于组件的application类需要实现IApplicationLike的接口,在IApplicationLike接口中定义onCreate和onStop两个生命周期方法,对应组件的加载和卸载。

1
2
3
4
5
6
7
8
9
10
11
public class ShareAppLike implements IApplicationLike {
Router router = Router.getInstance();
@Override
public void onCreate() {
router.addService(ShareService.class.getSimpleName(), new ShareServiceImpl());
}
@Override
public void onStop() {
router.removeService(ShareService.class.getSimpleName());
}
}

在app中面向ShareService来编程

1
2
3
4
5
6
7
Router router = Router.getInstance();
if (router.getService(ShareService.class.getSimpleName()) != null) {
ShareService service = (ShareService) router.getService(ShareService.class.getSimpleName());
fragment = service.getShareFragment();
ft = getSupportFragmentManager().beginTransaction();
ft.add(R.id.tab_content, fragment).commitAllowingStateLoss();
}

由于组件是动态加载和卸载的,在使用ShareService的需要进行判空处理。我们看到数据的传输是通过一个中央路由Router来实现的,这个Router就是一个HashMap

UI跳转

页面(activity)的跳转也是通过一个中央路由UIRouter来实现,这里增加了一个优先级的概念。
页面的跳转通过短链的方式,跳转到share的activity

1
UIRouter.getInstance().openUri(getActivity(), "componentdemo://share", null);

share组件在自己实现的ShareUIRouter中声明了自己处理这个短链(scheme和host)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static final String SCHEME = "componentdemo";
private static final String SHAREHOST = "share";
public boolean openUri(Context context, Uri uri, Bundle bundle) {
if (uri == null || context == null) {
return true;
}
String host = uri.getHost();
if (SHAREHOST.equals(host)) {
Intent intent = new Intent(context, ShareActivity.class);
intent.putExtras(bundle == null ? new Bundle() : bundle);
context.startActivity(intent);
return true;
}
return false;
}

如果已经组件已经响应了这个短链,就返回true,这样更低优先级的组件就不会接收到这个短链。注解生成根据scheme和host跳转的逻辑,如ARouter的开源框架已实现。

集成调试

由app或者其他组件充当host的角色,引入其他相关的组件一起参与编译,从而测试整个交互流程。app和组件都可以充当host的角色。在这里我们以app为例。

  1. 在根项目的gradle.properties中增加一个变量mainmodulename,其值就是工程中的主项目,这里是app。设置为mainmodulename的module,isRunAlone永远是true。
  2. 在app项目的gradle.properties文件中增加两个变量:
    1
    2
    debugComponent=com.mrzhang.share:sharecomponent
    compileComponent=sharecomponent
    其中debugComponent是debug的时候引入的组件,compileComponent是release下引入的组件。
    debugComponent引入的组件写法是不同的,组件引入支持两种语法,module直接引用module工程,modulePackage:module,使用componentrelease中已经发布的aar。
    在集成调试中,要引入的share组件是不需要把自己的isRunAlone修改为false的。一个application工程是不能直接引用(compile)另一个application工程的,所以app和组件都是isRunAlone=true在正常情况下是编译不过的。
    插件会自动识别当前要调试的具体是哪个组件,然后把其他组件修改为library工程,而且这个修改只在当次编译生效。

通过task来判断判断当前要运行的是app还是哪个组件,判断的规则如下:
assembleRelease → app
app:assembleRelease或者 :app:assembleRelease → app
sharecomponent:assembleRelease 或者:sharecomponent:assembleRelease→ sharecomponent
这样每个组件可以直接在Androidstudio中run,也可以使用命令进行打包,不需要修改任何配置,却可以自动引入依赖的组件,在开发中可以提高工作效率。

代码边界

依赖的组件集成到host中,本质还是使用compile project(…)或者compile modulePackage:module@aar。不直接在build.gradle中直接引入是为了组件之间的完全隔离,可以称之为代码边界。为了避免直接引入实现类来编程,绕过了面向接口编程的约束。
从task入手,只有在assemble任务的时候才进行compile引入。这样在代码的开发期间,组件是完全不可见的。具体的代码如下:

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
/**
* 自动添加依赖,只在运行assemble任务的才会添加依赖,在开发期间组件之间是完全无感知,做到完全隔离的关键
* module直接引用module工程,modulePackage:module,使用componentrelease中已经发布的aar
* @param assembleTask
* @param project
*/
private void compileComponents(AssembleTask assembleTask, Project project) {
String components;
if (assembleTask.isDebug) {
components = (String) project.properties.get("debugComponent")
} else {
components = (String) project.properties.get("compileComponent")
}
if (components == null || components.length() == 0) {
return;
}
String[] compileComponents = components.split(",")
if (compileComponents == null || compileComponents.length == 0) {
return;
}
for (String str : compileComponents) {
if (str.contains(":")) {
File file = project.file("../componentrelease/" + str.split(":")[1] + "-release.aar")
if (file.exists()) {
project.dependencies.add("compile", str + "-release@aar")
} else {
throw new RuntimeException(str + " not found ! maybe you should generate a new one ")
}
} else {
project.dependencies.add("compile", project.project(':' + str))
}
}
}

生命周期

集成调试,可以在打包的时候把依赖的组件参与编译,此时反编译apk的代码会看到各个组件的代码和资源都已经包含在包里面。但是由于每个组件的唯一入口ApplicationLike还没有执行oncreate()方法,所以组件并没有把自己的服务注册到中央路由,因此组件实际上是不可达的。

加载组件目前插件提供了两种方式,字节码插入和反射调用。

字节码插入模式

在dex生成之前,扫描所有的ApplicationLike类,它有一个共同的父类,然后通过javassist在主项目的Application.onCreate()中插入调用ApplicationLike.onCreate()的代码。这样就相当于每个组件在application启动的时候就加载起来了。

反射调用的方式

手动在Application.onCreate()中或者在其他合适的时机手动通过反射的方式来调用ApplicationLike.onCreate()。比起字节码插入,这种方式有两个好处,一是字节码插入对代码进行扫描和插入会增加编译的时间,在debug的时候会影响效率,并且这种模式对Instant Run支持不好;二是可以更灵活的控制加载或者卸载时机。
这两种模式的配置是通过配置插件的Extension来实现的,下面是字节码插入的模式下的配置格式,添加applicatonName的目的是加快定位Application的速度。

1
2
3
4
combuild {
applicatonName = 'com.mrzhang.component.application.AppApplication'
isRegisterCompoAuto = true
}

组件化集成步骤

以官方的demo为例子:

  1. 将项目中的所有第三方库引用添加到basiclib的build.gradle
    1
    2
    compile 'com.squareup.okhttp3:okhttp:3.4.1'
    compile 'com.squareup.picasso:picasso:2.5.2'
  2. 将项目中的公共资源如颜色,字符串,风格 ,主要是values里面的东西迁移到basicres
  3. 导入gradle插件
    ComExtension 是否自动注册管理
    ComBuild gradle task 对组件进行打包
    ComCodeTransform ConvertUtils 工具类
  4. componentlib 组件化的基础库 Router/UIRouter的实现
  5. componentservice 定义组件提供的服务接口声明,这里是阅读组件提供了一个fragment
    1
    2
    3
    public interface ReadBookService {
    Fragment getReadBookFragment();
    }
  6. readerComponent
    ui路由的注册,实现IApplicationLike接口,此处使用的是默认的ui路由
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class ReaderAppLike implements IApplicationLike {

    Router router = Router.getInstance();

    @Override
    public void onCreate() {
    router.addService(ReadBookService.class.getSimpleName(), new ReadBookServiceImpl());
    }

    @Override
    public void onStop() {
    router.removeService(ReadBookService.class.getSimpleName());
    }
    }

实现提供的ReadBookService接口

1
2
3
4
5
6
public class ReadBookServiceImpl implements ReadBookService {
@Override
public Fragment getReadBookFragment() {
return new ReaderFragment();
}
}

需要提供的fragment实现,点击后跳转到share的activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ReaderFragment extends Fragment {

private View rootView;

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
if (rootView == null) {
rootView = inflater.inflate(R.layout.readerbook_fragment_reader, container,
false);
rootView.findViewById(R.id.tv_content).setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
UIRouter.getInstance().openUri(getActivity(), "componentdemo://share", null);
}
});
}
return rootView;
}
}
  1. shareComponent
    ui路由的注册,实现IApplicationLike接口,使用的是自定义的ui路由
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class ShareApplike implements IApplicationLike {

    UIRouter uiRouter = UIRouter.getInstance();
    ShareUIRouter shareUIRouter = ShareUIRouter.getInstance();

    @Override
    public void onCreate() {
    uiRouter.registerUI(shareUIRouter);
    }

    @Override
    public void onStop() {
    uiRouter.unregisterUI(shareUIRouter);
    }
    }
    自定义ui路由,实现IComponentRouter接口
    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
    public class ShareUIRouter implements IComponentRouter {

    private static final String SCHME = "componentdemo";

    private static final String SHAREHOST = "share";

    private static String[] HOSTS = new String[]{SHAREHOST};

    private static ShareUIRouter instance = new ShareUIRouter();

    private ShareUIRouter() {

    }

    public static ShareUIRouter getInstance() {
    return instance;
    }

    @Override
    public boolean openUri(Context context, String url, Bundle bundle) {
    if (TextUtils.isEmpty(url) || context == null) {
    return true;
    }
    return openUri(context, Uri.parse(url), bundle);
    }

    @Override
    public boolean openUri(Context context, Uri uri, Bundle bundle) {
    if (uri == null || context == null) {
    return true;
    }
    String host = uri.getHost();
    if (SHAREHOST.equals(host)) {
    Intent intent = new Intent(context, ShareActivity.class);
    intent.putExtras(bundle == null ? new Bundle() : bundle);
    context.startActivity(intent);
    return true;
    }
    return false;
    }

    @Override
    public boolean verifyUri(Uri uri) {
    String scheme = uri.getScheme();
    String host = uri.getHost();
    if (SCHME.equals(scheme)) {
    for (String str : HOSTS) {
    if (str.equals(host)) {
    return true;
    }
    }
    }
    return false;
    }
    }

activity的具体实现

1
2
3
4
5
6
7
8
9
public class ShareActivity extends AppCompatActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.share_activity_share);
}

}

最后在主app中调用:
使用shareComponent提供的fragment

1
2
3
4
5
6
7
Router router = Router.getInstance();
if (router.getService(ReadBookService.class.getSimpleName()) != null) {
ReadBookService service = (ReadBookService) router.getService(ReadBookService.class.getSimpleName());
fragment = service.getReadBookFragment();
ft = getSupportFragmentManager().beginTransaction();
ft.add(R.id.tab_content, fragment).commitAllowingStateLoss();
}

注册activity

1
Router.registerComponent("com.mrzhang.share.applike.ShareApplike");

Router实现解析

不管是哪个组件化方案,路由技术都是必不可少的,得到团队的这个方案路由未采用第三方,且实现了动态加载和卸载,确实不错。

  1. 定义组件ui路由接口,组件自定义ui路由需实现这个接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public interface IComponentRouter {

    /**
    * 打开一个链接
    *
    * @param url
    * 目标url可以是http 或者 自定义scheme
    * @param bundle
    * 打开目录activity时要传入的参数。建议只传基本类型参数。
    * @return 是否正常打开
    */
    public boolean openUri(Context context, String url, Bundle bundle);

    public boolean openUri(Context context,Uri uri, Bundle bundle);

    public boolean verifyUri(Uri uri);
    }
  2. 实现一个统一管理ui路由的接口,继承IComponentRouter
    设置优先级和提供注册方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public interface IUIRouter extends IComponentRouter {

    int PRIORITY_NORMAL = 0;
    int PRIORITY_LOW = -1000;
    int PRIORITY_HEIGHT = 1000;

    void registerUI(IComponentRouter router, int priority);

    void registerUI(IComponentRouter router);

    void unregisterUI(IComponentRouter router);
    }
  3. 实现一个统一管理的ui路由
    定义了一个路由列表和优先级map
    1
    2
    List<IComponentRouter> uiRouters = new ArrayList<IComponentRouter>();
    HashMap<IComponentRouter, Integer> priorities = new HashMap<IComponentRouter, Integer>();
    提供单例对象的接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private UIRouter() {
    }
    public static UIRouter getInstance() {
    if (instance == null) {
    synchronized (UIRouter.class) {
    if (instance == null) {
    instance = new UIRouter();
    }
    }
    }
    return instance;
    }
    注册一个ui路由并设置优先级,如果当前列表存在这个路由且优先级相同则返回,移除已存在的路由对象,遍历路由列表,当优先级小于当前路由时,退出遍历并插入列表,同时插入优先级到优先级map
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Override
    public void registerUI(IComponentRouter router, int priority) {
    if (priorities.containsKey(router) && priority == priorities.get(router)) {
    return;
    }
    removeOldUIRouter(router);
    int i = 0;
    for (IComponentRouter temp : uiRouters) {
    Integer tp = priorities.get(temp);
    if (tp == null || tp <= priority) {
    break;
    }
    i++;
    }
    uiRouters.add(i, router);
    priorities.put(router, Integer.valueOf(priority));
    }
    反注册执行移除操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Override
    public void unregisterUI(IComponentRouter router) {
    for (int i = 0; i < uiRouters.size(); i++) {
    if (router == uiRouters.get(i)) {
    uiRouters.remove(i);
    priorities.remove(router);
    break;
    }
    }
    }
    统一对ui理由的url进行处理,没有特殊前缀加上http头返回
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Override
    public boolean openUri(Context context, String url, Bundle bundle) {
    url = url.trim();
    if (!TextUtils.isEmpty(url)) {
    if (!url.contains("://") &&
    (!url.startsWith("tel:") ||
    !url.startsWith("smsto:") ||
    !url.startsWith("file:"))) {
    url = "http://" + url;
    }
    Uri uri = Uri.parse(url);
    return openUri(context, uri, bundle);
    }
    return true;
    }
    遍历路由列表,调用当前ui路由的检查方法和跳转方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Override
    public boolean openUri(Context context, Uri uri, Bundle bundle) {
    for (IComponentRouter temp : uiRouters) {
    try {
    if (temp.verifyUri(uri) && temp.openUri(context, uri, bundle)) {
    return true;
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    return false;
    }
  4. 路由实现类Router,负责组件的服务管理和组件注册,同时调用applicationLike的onCreate和onStop,相当于Application的初始化与销毁的生命周期
    组件的服务map和注册组件map
    1
    2
    3
    private HashMap<String, Object> services = new HashMap<>();
    //注册组件的集合
    private static HashMap<String, IApplicationLike> components = new HashMap<>();
    提供单例对象的接口:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    private Router() {
    }

    public static Router getInstance() {
    if (instance == null) {
    synchronized (Router.class) {
    if (instance == null) {
    instance = new Router();
    }
    }
    }
    return instance;
    }
    组件服务的添加移除和获取
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public synchronized void addService(String serviceName, Object serviceImpl) {
    if (serviceName == null || serviceImpl == null) {
    return;
    }
    services.put(serviceName, serviceImpl);
    }

    public synchronized Object getService(String serviceName) {
    if (serviceName == null) {
    return null;
    }
    return services.get(serviceName);
    }

    public synchronized void removeService(String serviceName) {
    if (serviceName == null) {
    return;
    }
    services.remove(serviceName);
    }
    组件的注册与反注册,调用此处可实现动态的加载和卸载组件
    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
    /**
    * 注册组件
    *
    * @param classname 组件名
    */
    public static void registerComponent(@Nullable String classname) {
    if (TextUtils.isEmpty(classname)) {
    return;
    }
    if (components.keySet().contains(classname)) {
    return;
    }
    try {
    Class clazz = Class.forName(classname);
    IApplicationLike applicationLike = (IApplicationLike) clazz.newInstance();
    applicationLike.onCreate();
    components.put(classname, applicationLike);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    /**
    * 反注册组件
    *
    * @param classname 组件名
    */
    public static void unregisterComponent(@Nullable String classname) {
    if (TextUtils.isEmpty(classname)) {
    return;
    }
    if (components.keySet().contains(classname)) {
    components.get(classname).onStop();
    components.remove(classname);
    return;
    }
    try {
    Class clazz = Class.forName(classname);
    IApplicationLike applicationLike = (IApplicationLike) clazz.newInstance();
    applicationLike.onStop();
    components.remove(classname);
    } catch (Exception e) {
    e.printStackTrace();
    }
    }


Architecture Components 是在 2017 年 Google I/O 大会上,Google 官方推出的一个构建 Android 应用架构的库。它可以帮你避免在 Android 应用开发中常见的一些问题,比如:内存泄露,管理组件生命周期等等,帮助你去构建一个健壮,易测,可维护的应用。Now available in preview目前的版本还是一个预览版,但是我们依然可以去了解一下。

Google认为在 Android 中,应用间的这种跳跃切换行为很普遍,应用程序必须把这些问题都正确的处理好。移动设备的硬件资源是很有限的,在任何时候系统都可能杀掉一些应用去释放一些资源给新的应用。那么你的应用组件的创建和销毁是不完全可控的,它可能在任何时候由于用户或者系统的行为而触发。应用组件的生命周期也不是完全由你掌控的,所以不应该存储一些数据或者状态在应用组件中,应用组件之间也不应该彼此依赖。


架构原则

  1. 应用中的关注点分离。不应该在一个Activity或者Fragment中写所有的代码。任何与 UI 或者交互无关的代码都不应该存在这些类中。保证他们尽可能的职责单一化将会使我们避免很多生命周期相关的问题。因为Android系统可能会随时由于用户的行为或者系统状态(比如剩余内存过低)而销毁你的应用组件,减少组件之间的依赖可以提供一个稳定的用户体验。
  2. 应该采用Model驱动UI的方式,最好是一个可持久化的模型。满足以下两个条件:
    • 如果系统销毁我们的应用,用户不会为此而导致丢失数据。
    • 在网络状况不好甚至断网的情况下,我们的应用仍然可以继续工作。

Model是专门负责为我们的应用处理和存储数据的。完全独立于View和其他应用中的组件,所以不存在生命周期相关的问题。保证UI部分的代码足够简单,没有业务逻辑,使Model管理数据的职责明确,这样更容易测试,而且稳定性更高。

框架介绍

生命周期的处理

在 android.arch.lifecycle包中提供了生命周期感知的组件的类和接口,这些组件可以根据 Activity/Fragment 的生命周期自动调整它的行为。

  • LifecycleOwner:是一个具有单一方法的接口。如果一个类实现了此接口,则该类中需要持有一个Lifecycle对象,并通过 LifecycleOwner#getLifecycle()方法返回该对象。代码如下:
    1
    2
    3
    4
    5
    private final LifecycleRegistry mRegistry = new LifecycleRegistry(this);
    @Override
    public LifecycleRegistry getLifecycle() {
    return mRegistry;
    }
    如果我们需要在Activity的生命周期的某些状态中对一些工具类做对应的处理,按以前的写法:
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
class MyListener {
public MyListener(Callback callback) {
// ...
}

void start() {
// ...
}

void stop() {
// ...
}

void enable(){
// ...
}
public interface Callback{
void onLocation(long longtitude ,long latitude);
}
}

class MyActivity extends AppCompatActivity {
private MyListener myListener;

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myListener = new MyListener(new MyListener.Callback() {
@Override
public void onLocation(long longtitude, long latitude) {
Log.d(TAG, "onLocation: " +longtitude+","+latitude);
}
});
}

public void onStart() {
super.onStart();
myListener.start();
}

public void onStop() {
super.onStop();
myListener.stop();
}
}

定义一个位置监听类MyLocationListener,定义它在MyActivity生命周期的onStart和onStop中需要执行的方法,start()和stop(),接着在MyActivity的对应状态中调用。

如果我们让工具类实现LifecycleObserver接口,我们需要注入MyActivity的生命周期对象Lifecycle,那么我们只需要增加注解就可以实现在对应状态调用。

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
public class MyListener implements LifecycleObserver{
public static final String TAG="MyListener";

private boolean enabled = false;
private Callback callback;
private Lifecycle lifecycle;
public MyListener(Lifecycle lifecycle, Callback callback) {
this.lifecycle=lifecycle;
this.callback =callback;
}

@OnLifecycleEvent(Lifecycle.Event.ON_START)
void start(){
Log.d(TAG, "start: ");
}

@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void stop(){
Log.d(TAG, "stop: ");

}


public void enable() {
enabled = true;
if(lifecycle.getCurrentState().isAtLeast(Lifecycle.State.INITIALIZED)){
getLocation();
}
}

//模拟位置获取
private void getLocation(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long longtitude = (long) (Math.random()*100);
long latitude = (long) (Math.random()*100);
callback.onLocation(longtitude,latitude);
}

public interface Callback{
void onLocation(long longtitude ,long latitude);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyActivity extends LifecycleActivity {
private MyListener myListener;
public static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myListener = new MyListener(this, getLifecycle(), new MyListener.Callback() {
@Override
public void onLocation(long longtitude, long latitude) {
Log.d(TAG, "onLocation: " +longtitude+","+latitude);
}
});
getLifecycle().addObserver(myListener);
myListener.enable();
}
}

在onStart中调用就增加@OnLifecycleEvent(Lifecycle.Event.ON_START)的注解,不用在MyActivity中调用了。
MyActivity继承了LifecycleActivity,而LifecycleActivity已经实现了LifecycleOwner的接口,这里我们需要通过getLifecycle()传入MyActivity的生命周期对象Lifecycle到工具类中即可。
工具类获得Lifecycle对象后可对MyActivity的周期状态进行判断,Lifecycle.State.INITIALIZED这个代表生命周期所有者的初始化状态。
当然还有其他状态CREATED DESTROYED RESUMED STARTED

目前这个库只是一个beta版,提供的api一直都在变动,这里就通过一个入门的小例子来学习一下。

这个小例子是展示知乎的一个资讯列表。

目前有两个可用的免费api

https://news-at.zhihu.com/api/4/news/latest

https://news-at.zhihu.com/api/4/news/before/{date}

先创建一个知乎activity,这个相当于mvp架构的v层,代码如下:

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
public class ZhihuActivity extends LifecycleActivity{

private static final String TAG = "ZhihuActivity";
private SwipeRefreshLayout refreshLayout = null;
private ZhihuListAdapter adapter = null;
private ProgressBar loadMorebar = null;
private View rootview = null;

private ZhihuListViewModel zhihuListViewModel = null;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_zhihu);
initView();
subscribeUI();
}

//初始化view
private void initView(){
rootview=findViewById(R.id.rl_zhihu_root);
loadMorebar=findViewById(R.id.bar_load_more_zhihu);
refreshLayout=findViewById(R.id.refresh);
RecyclerView recyclerView = findViewById(R.id.rv_zhihu_list);

LinearLayoutManager layoutManager = new LinearLayoutManager(this);
adapter = new ZhihuListAdapter(this);
adapter.setClickListener(new OnItemClickListener<ZhihuStory>() {
@Override
public void onClick(ZhihuStory zhihuStory) {

}
});

recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(layoutManager);
recyclerView.addOnScrollListener(new ZhihuOnScrollListener());

refreshLayout.setOnRefreshListener(new ZhihuSwipeListener());
refreshLayout.setColorSchemeResources(
android.R.color.holo_blue_bright,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light);

}

private void subscribeUI() {
// 通过 ViewModelProviders 创建对应的 ZhihuListViewModel 对象
ZhihuListViewModel.Factory factory = new ZhihuListViewModel
.Factory(getApplication()
, Injection.getDataRepository(getApplication()));
zhihuListViewModel = ViewModelProviders.of(this, factory).get(ZhihuListViewModel.class);
//注册知乎列表数据的监听,当有改变时刷新列表
zhihuListViewModel.getZhihuList().observe(this, new Observer<List<ZhihuStory>>() {
@Override
public void onChanged(@Nullable List<ZhihuStory> stories) {
if (stories == null || stories.size() <= 0) {
return;
}
Log.i(TAG,"size is " + stories.size());
adapter.setStoryList(stories);
}
});
//注册加载更多的监听
zhihuListViewModel.isLoadingZhihuList().observe(this, new Observer<Boolean>() {
@Override
public void onChanged(@Nullable Boolean aBoolean) {
if (aBoolean == null) {
return;
}
Log.i(TAG,"state " + aBoolean);
refreshLayout.setRefreshing(false);
loadMorebar.setVisibility(aBoolean ? View.VISIBLE : View.INVISIBLE);
}
});
zhihuListViewModel.refreshZhihusData();
}

/**
* 下拉刷新回调
*/
private class ZhihuSwipeListener implements SwipeRefreshLayout.OnRefreshListener {
@Override
public void onRefresh() {
adapter.clearStoryList();
zhihuListViewModel.refreshZhihusData();
}
}

/**
* 上拉加载更多回调
*/
class ZhihuOnScrollListener extends RecyclerView.OnScrollListener {

@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
LinearLayoutManager layoutManager = (LinearLayoutManager)
recyclerView.getLayoutManager();
int lastPosition = layoutManager
.findLastCompletelyVisibleItemPosition();
if (lastPosition == adapter.getItemCount() - 1) {
// 上拉加载更多数据
zhihuListViewModel.loadNextPageZhihu(lastPosition,getApplicationContext());
}
}
}
}

recyclerView的adapter很简单,直接上代码:

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
public class ZhihuListAdapter extends RecyclerView.Adapter<ZhihuListAdapter.ZhihuViewHolder> {

private OnItemClickListener<ZhihuStory> clickListener = null;

private Context mContext = null;

private List<ZhihuStory> mStoryList = null;

public ZhihuListAdapter(Context context) {
mStoryList = new ArrayList<>();
mContext = context;
}

public void setClickListener(OnItemClickListener<ZhihuStory> listener){
clickListener = listener;
}
@Override
public ZhihuViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ZhihuViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.zhihu_list_item, parent, false));
}

@Override
public void onBindViewHolder(ZhihuViewHolder holder, int position) {
final ZhihuStory zhihuStory = mStoryList.get(position);
holder.tv_zhihu_title.setText(zhihuStory.getTitle());
holder.tv_zhihu_time.setText(zhihuStory.getGa_prefix());
holder.ll_zhihu.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(clickListener != null){
clickListener.onClick(zhihuStory);
}
}
});
Glide.with(mContext)
.load(zhihuStory.getImages()[0])
.centerCrop()
.into(holder.img_zhihu);
}

@Override
public int getItemCount() {
return mStoryList.size();
}

//数据添加后刷新列表
public void setStoryList(List<ZhihuStory> storyList) {
if (storyList == null || storyList.size() == 0) {
return;
}
mStoryList.addAll(storyList);
notifyDataSetChanged();
}

//清空当前数据
public void clearStoryList() {
mStoryList.clear();
notifyDataSetChanged();
}

class ZhihuViewHolder extends RecyclerView.ViewHolder {

public View ll_zhihu;
public TextView tv_zhihu_title;
public TextView tv_zhihu_time;
public ImageView img_zhihu;

private ZhihuViewHolder(View itemView) {
super(itemView);
ll_zhihu = itemView.findViewById(R.id.ll_zhihu);
tv_zhihu_title = itemView.findViewById(R.id.tv_zhihu_title);
tv_zhihu_time = itemView.findViewById(R.id.tv_zhihu_time);
img_zhihu = itemView.findViewById(R.id.img_zhihu);
}
}
}

对应的item监听代码如下:

1
2
3
public interface OnItemClickListener<T> {
void onClick(T t);
}

对应的布局文件代码如下:

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_zhihu"
android:layout_width="match_parent"
android:layout_height="113dp">

<ImageView
android:id="@+id/img_zhihu"
android:layout_width="112dp"
android:layout_height="112dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_gravity="center_vertical"
android:contentDescription="@null"
android:paddingBottom="16dp"
android:paddingEnd="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:src="@mipmap/ic_launcher"/>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="112dp"
android:layout_toEndOf="@+id/img_zhihu"
android:layout_toRightOf="@+id/img_zhihu"
android:orientation="vertical">

<TextView
android:id="@+id/tv_zhihu_title"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingLeft="16dp"
android:paddingStart="16dp"
android:paddingTop="24dp"/>

<TextView
android:id="@+id/tv_zhihu_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:paddingLeft="16dp"
android:paddingStart="16dp"/>
</LinearLayout>

<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_alignParentBottom="true"
android:layout_marginLeft="112dp"
android:layout_marginStart="112dp"
android:background="@color/lightGrey"/>
</RelativeLayout>

这里创建了ZhihuListViewModel,相当于mvp架构的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

public class ZhihuListViewModel extends AndroidViewModel {

// 请求接口中查询的日期参数
private MutableLiveData<String> requestPageDate = new MutableLiveData<>();
// 知乎列表的数据
private final LiveData<List<ZhihuStory>> mZhihuList;
// 数据源对象
private DataRepository dataRepository = null;

private ZhihuListViewModel(Application application, final DataRepository dataRepository) {
super(application);
this.dataRepository = dataRepository;
// 使用 Transformations.switchMap() 方法,当 View 改变 requestPageDate 参数的值时,则进行 zhihu 列表数据的请求
mZhihuList = Transformations.switchMap(requestPageDate, new Function<String, LiveData<List<ZhihuStory>>>() {
@Override
public LiveData<List<ZhihuStory>> apply(String input) {
return dataRepository.getZhihuList(input);
}
});
}

/**
* 获取 Zhihu 列表数据
*
* @return Zhihu 列表数据
*/
public LiveData<List<ZhihuStory>> getZhihuList() {
return mZhihuList;
}

/**
* 数据请求状态由 DataRepository 控制,包括下拉刷新和上拉加载更多
*
* @return 是否在进行数据请求
*/
public LiveData<Boolean> isLoadingZhihuList() {
return dataRepository.isLoadingZhihuList();
}

/**
* 下拉刷新,获取最新的 Zhihu 列表数据
*/
public void refreshZhihusData() {
requestPageDate.setValue("today");
}

/**
* 上拉加载更多时,获取 Zhihu 历史列表数据
*
* @param positon 表示列表滑动到最后一项
*/
public void loadNextPageZhihu(int positon, Context context) {
if (!Util.isNetworkConnected(context)) {
return;
}
requestPageDate.setValue(String.valueOf(positon));
}

public static class Factory extends ViewModelProvider.NewInstanceFactory {

@NonNull
private final Application application;

private final DataRepository dataRepository;

public Factory(@NonNull Application application, DataRepository dataRepository) {
this.application = application;
this.dataRepository = dataRepository;
}

@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
return (T) new ZhihuListViewModel(application, dataRepository);
}
}
}

初始化时,对requestPageDate做监听,当requestPageDate的值有变化时,调用getZhihuList刷新列表的数据。列表刷新时回调中会调用refreshZhihusData(),此时会改变requestPageDate的值,也就会调用getZhihuList。列表加载更多时回调中会调用loadNextPageZhihu(int positon, Context context) ,也会改变requestPageDate的值,同样会调用getZhihuList。

接下来是DataRepository,数据源管理类,对应mvp的m层。

通过一个类注入数据源对象

1
2
3
4
5
6
public class Injection {
public static DataRepository getDataRepository(Application application) {
return DataRepository.getInstance(RemoteDataSource.getInstance(),
LocalDataSource.getInstance(application), application);
}
}

DataRepository单例类对网络数据源和本地数据源做统一管理

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
public class DataRepository {

private final DataSource remoteDataSource;//网络数据源
private final DataSource localDataSource;//本地数据源
private static DataRepository INSTANCE = null;
private static Application application = null;

private DataRepository(@NonNull DataSource remoteDataSource,
@NonNull DataSource localDataSource) {
this.remoteDataSource = remoteDataSource;
this.localDataSource = localDataSource;
}

static DataRepository getInstance(@NonNull DataSource remoteDataSource,
@NonNull DataSource localDataSource,
Application app) {
if (INSTANCE == null) {
synchronized (DataRepository.class) {
if (INSTANCE == null) {
INSTANCE = new DataRepository(remoteDataSource, localDataSource);
application = app;
}
}
}
return INSTANCE;
}

//获取知乎列表的数据
public LiveData<List<ZhihuStory>> getZhihuList(@NonNull String date) {
//联网时使用网络数据源
if (Util.isNetworkConnected(application.getApplicationContext())) {
//第一页获取最近的知乎资讯列表
if (date.equals("today")) {
return remoteDataSource.getLastZhihuList();
}
//获取更多知乎资讯
else {
return remoteDataSource.getMoreZhihuList();
}
}
//断网时使用本地数据源
else {
if (date.equals("today")) {
return localDataSource.getLastZhihuList();
} else {
return localDataSource.getMoreZhihuList();
}
}
}

//加载状态
public LiveData<Boolean> isLoadingZhihuList() {
//联网时返回网络数据源加载状态
if (Util.isNetworkConnected(application.getApplicationContext())) {
return remoteDataSource.isLoadingZhihuList();
}
//断网时返回本地数据源加载状态
else {
return localDataSource.isLoadingZhihuList();
}
}
}

判断网络状态工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Util {
public static boolean isNetworkConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null) {
return mNetworkInfo.isAvailable();
}
}
return false;
}
}

网络数据源和本地数据源的统一接口

1
2
3
4
5
public interface DataSource {
LiveData<List<ZhihuStory>> getLastZhihuList();//加载最近的资讯列表
LiveData<List<ZhihuStory>> getMoreZhihuList();//列表加载更多资讯
LiveData<Boolean> isLoadingZhihuList();//获取加载状态
}

网络数据源单例类对网络请求做统一管理

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
public class RemoteDataSource  implements DataSource {
private static RemoteDataSource INSTANCE = null;
private final MutableLiveData<Boolean> isLoadingZhihulist; //列表加载标记
private final MutableLiveData<List<ZhihuStory>> zhihuList; //列表数据
private String requestDate;//请求日期
private final ApiZhihu apiZhihu;//请求api对象

{
isLoadingZhihulist = new MutableLiveData<>();
zhihuList = new MutableLiveData<>();
}

private RemoteDataSource() {
apiZhihu = ApiManager.getInstance().getApiZhihu();
}

public static RemoteDataSource getInstance() {
if (INSTANCE == null) {
synchronized (RemoteDataSource.class) {
if (INSTANCE == null) {
INSTANCE = new RemoteDataSource();
}
}
}
return INSTANCE;
}

@Override
public LiveData<List<ZhihuStory>> getLastZhihuList() {
isLoadingZhihulist.setValue(true);//加载开始
apiZhihu.getLatestNews()
.enqueue(new Callback<ZhihuData>() {
@Override
public void onResponse(Call<ZhihuData> call, Response<ZhihuData> response) {
if (response.isSuccessful()) {
zhihuList.setValue(response.body().getStories());
refreshLocalZhihuList(response.body().getStories());
requestDate = response.body().getDate();
}
isLoadingZhihulist.setValue(false);//加载结束
}

@Override
public void onFailure(Call<ZhihuData> call, Throwable t) {
isLoadingZhihulist.setValue(false);//加载结束
}
});
return zhihuList;
}

@Override
public LiveData<List<ZhihuStory>> getMoreZhihuList() {
isLoadingZhihulist.setValue(true);//加载开始
apiZhihu.getTheDaily(requestDate)
.enqueue(new Callback<ZhihuData>() {
@Override
public void onResponse(Call<ZhihuData> call, Response<ZhihuData> response) {
if (response.isSuccessful()) {
zhihuList.setValue(response.body().getStories());
refreshLocalZhihuList(response.body().getStories());
requestDate = response.body().getDate();
}
isLoadingZhihulist.setValue(false);//加载结束
}

@Override
public void onFailure(Call<ZhihuData> call, Throwable t) {
isLoadingZhihulist.setValue(false);//加载结束
}
});
return zhihuList;
}

//返回加载状态
@Override
public MutableLiveData<Boolean> isLoadingZhihuList() {
return isLoadingZhihulist;
}

//刷新本地数据
private void refreshLocalZhihuList(List<ZhihuStory> zhihuStoryList) {
if (zhihuStoryList == null || zhihuStoryList.isEmpty()) {
return;
}
AppDatabaseManager.getInstance().insertZhihuList(zhihuStoryList);
}
}

每次网络请求后,都需要刷新本地数据源的数据,ApiManager对api做统一管理,提供获取Retrofit网络请求对象的接口。

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 ApiManager {
//全局url
private static final String ZHIHU_URL = "https://news-at.zhihu.com/";

private static ApiManager INSTANCE;
//Retrofit网络请求对象
private static ApiZhihu apiZhihu;

private ApiManager() {
}

public static ApiManager getInstance() {
if (INSTANCE == null) {
synchronized (ApiManager.class) {
if (INSTANCE == null) {
INSTANCE = new ApiManager();
}
}
}
return INSTANCE;
}

//获取Retrofit对象,为空时初始化实例
public ApiZhihu getApiZhihu() {
if (apiZhihu == null) {
synchronized (ApiManager.class) {
if (apiZhihu == null) {
apiZhihu = new Retrofit.Builder()
.baseUrl(ZHIHU_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiZhihu.class);
}
}
}
return apiZhihu;
}
}

定义Retrofit网络请求接口,代码如下:

1
2
3
4
5
6
7
public interface ApiZhihu {
@GET("api/4/news/latest")
Call<ZhihuData> getLatestNews();

@GET("/api/4/news/before/{date}")
Call<ZhihuData> getTheDaily(@Path("date") String date);
}

本地数据源单例类做统一管理

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
public class LocalDataSource implements DataSource {

private static LocalDataSource INSTANCE = null;

private LocalDataSource(Context context) {
AppDatabaseManager.getInstance().createDB(context);
}

public static LocalDataSource getInstance(Context context) {
if (INSTANCE == null) {
synchronized (LocalDataSource.class) {
if (INSTANCE == null) {
INSTANCE = new LocalDataSource(context);
}
}
}
return INSTANCE;
}

@Override
public LiveData<List<ZhihuStory>> getLastZhihuList() {
return AppDatabaseManager.getInstance().loadZhihuList();
}

@Override
public LiveData<List<ZhihuStory>> getMoreZhihuList() {
return null;
}


@Override
public LiveData<Boolean> isLoadingZhihuList() {
return AppDatabaseManager.getInstance().isLoadingZhihuList();
}
}

AppDatabaseManager单例类是数据库的管理工具类,代码如下:

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
public class AppDatabaseManager {
private static final String DATABASE_NAME = "google-architecture-study-db";
private final MutableLiveData<Boolean> mIsLoadingZhihuList;
private final MutableLiveData<List<ZhihuStory>> mZhihuList;
private static AppDatabaseManager INSTANCE = null;
private AppDatabase mDB = null;

{
mIsLoadingZhihuList = new MutableLiveData<>();
mZhihuList = new MutableLiveData<>();
}

private AppDatabaseManager() {
}

public static AppDatabaseManager getInstance() {
if (INSTANCE == null) {
synchronized (AppDatabaseManager.class) {
if (INSTANCE == null) {
INSTANCE = new AppDatabaseManager();
}
}
}
return INSTANCE;
}

//创建数据库
public void createDB(Context context) {
new AsyncTask<Context, Void, Void>() {
@Override
protected Void doInBackground(Context... params) {
Context context = params[0].getApplicationContext();
mDB = Room.databaseBuilder(context,
AppDatabase.class, DATABASE_NAME).build();
return null;
}
}.execute(context.getApplicationContext());
}

//刷新本地知乎列表数据
public void insertZhihuList(final List<ZhihuStory> zhihuStoryList) {
new AsyncTask<Void, Void, Void>(){
@Override
protected Void doInBackground(Void... voids) {
mDB.beginTransaction();
try {
mDB.zhihuDao().insertZhihuList(zhihuStoryList);
mDB.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
mDB.endTransaction();
}
return null;
}
}.execute();
}

//知乎列表数据查询
public LiveData<List<ZhihuStory>> loadZhihuList() {
mIsLoadingZhihuList.setValue(true);
new AsyncTask<Void, Void, List<ZhihuStory>>() {
@Override
protected List<ZhihuStory> doInBackground(Void... voids) {
List<ZhihuStory> results = new ArrayList<>();
mDB.beginTransaction();
try {
results.addAll(mDB.zhihuDao().loadAllZhihus());
mDB.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
mDB.endTransaction();
}
return results;
}

@Override
protected void onPostExecute(List<ZhihuStory> aVoid) {
super.onPostExecute(aVoid);
mIsLoadingZhihuList.setValue(false);
mZhihuList.setValue(aVoid);
}

@Override
protected void onCancelled(List<ZhihuStory> aVoid) {
super.onCancelled(aVoid);
mIsLoadingZhihuList.setValue(false);
}
}.execute();
return mZhihuList;
}

//获取加载状态
public MutableLiveData<Boolean> isLoadingZhihuList() {
return mIsLoadingZhihuList;
}
}

初始化数据库room

1
2
3
4
5
@Database(entities = {ZhihuStory.class}, version = 1, exportSchema = false)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract ZhihuDao zhihuDao();
}

定义数据库room相关接口

1
2
3
4
5
6
7
8
@Dao
public interface ZhihuDao {
@Query("SELECT * FROM zhihustorys")
List<ZhihuStory> loadAllZhihus();

@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertZhihuList(List<ZhihuStory> zhihuStories);
}

时间字符串拼接与切割转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Converters {
@TypeConverter
public static String fromTimestamp(String[] arrays) {
StringBuilder stringBuilder = new StringBuilder();
for (String s : arrays) {
stringBuilder.append(s).append(",");
}
int length = stringBuilder.toString().length();
if (length > 0) {
stringBuilder = stringBuilder.deleteCharAt(length - 1);
}
return stringBuilder.toString();
}

@TypeConverter
public static String[] dateToTimestamp(String string) {
return string.split(",");
}
}

涉及到的dto,知乎资讯内层dto,需要增加一些注解,这里对应数据库的zhihustorys的表,主键为id,代码如下:

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
@Entity(tableName = "zhihustorys")
public class ZhihuStory {

@PrimaryKey
private String id;

private int type;

private String ga_prefix;

private String title;

private String[] images;

public ZhihuStory() {
}

public ZhihuStory(ZhihuStory zhihuStory) {
this.id = zhihuStory.getId();
this.type = zhihuStory.getType();
this.ga_prefix = zhihuStory.getGa_prefix();
this.title = zhihuStory.getTitle();
this.images = zhihuStory.getImages();
}

public String[] getImages() {
return images;
}

public void setImages(String[] images) {
this.images = images;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public int getType() {
return type;
}

public void setType(int type) {
this.type = type;
}

public String getGa_prefix() {
return ga_prefix;
}

public void setGa_prefix(String ga_prefix) {
this.ga_prefix = ga_prefix;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

知乎资讯外层dto,代码如下:

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
public class ZhihuData {
private String date;

private List<ZhihuStory> stories;

private List<ZhihuStory> top_stories;

public String getDate() {
return date;
}

public void setDate(String date) {
this.date = date;
}

public List<ZhihuStory> getStories() {
return stories;
}

public void setStories(List<ZhihuStory> stories) {
this.stories = stories;
}

public List<ZhihuStory> getTop_stories() {
return top_stories;
}

public void setTop_stories(List<ZhihuStory> top_stories) {
this.top_stories = top_stories;
}
}

以上是基于Architecture Components 架构的展示知乎资讯列表的所有代码。

总结:Architecture Components的架构总体上看起来还是很像MVP+Rxjava,M层对应DataRepository,V层对应activity,P层对应AndroidViewModel。AndroidViewModel中注册了请求参数改变的监听,在回调中发起数据请求,V层调用P层是通过AndroidViewModel的方法修改对应的请求参数。activity中注册了AndroidViewModel中的列表数据改变监听,在回调中刷新列表控件,P层得到结果刷新V层,也是通过改变AndroidViewModel中的列表数据来触发回调刷新。所以P层和V层都没有抽象出接口。M层通过统一的数据源管理类分别管理网络数据源和本地数据源。网络数据源通过Retrofit做网络请求,本地数据源主要通过数据库存储实现,定义了一个room数据库工具类做统一管理。个人觉得就目前的版本而言,如果原本就是MVP+Rxjava这种架构的完全可以不用重构了。


Databinding 在2015年7月发布的Android Studio v1.3.0 版本上引入,在2016年4月Android Studio v2.0.0 上正式支持。目前为止,已经支持双向绑定。

在android中实现 MVVM 的架构模式,Data binding 是最具代表性的框架,Google推出这一框架的原因是通过数据绑定技术可以解决界面与业务分离问题,对应的在Android中你不必再写什么findViewById()、setText(),setVisibility(),setEnabled() 或 setOnClickListener() 等方法了,可简化代码。

由于以前缺少对架构的关注和重视,虽然这个框架很早出来,但是也没有进行系统性的学习,下面就对Data binding的使用做一个总结。


添加依赖

在app下的build.gradle文件中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
//...
}
buildTypes {
//...
}
dataBinding {
enabled true
}
}

基础用法

定义数据对象

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
public class TestNormalDto {
private String test1;
private String test2;
private String test3;

public TestNormalDto(String test1, String test2, String test3) {
this.test1 = test1;
this.test2 = test2;
this.test3 = test3;
}

public String getTest1() {
return test1;
}

public void setTest1(String test1) {
this.test1 = test1;
}

public String getTest2() {
return test2;
}

public void setTest2(String test2) {
this.test2 = test2;
}

public String getTest3() {
return test3;
}

public void setTest3(String test3) {
this.test3 = test3;
}
}

布局文件

在 DataBinding 中,xml 的布局文件就不再单纯的展示UI,还需要定义 UI用到的变量,将变量直接与UI捆绑,个人认为类似于web开发的响应式写法。所以,它的根节点不再是一个 ViewGroup,而是变成了·layout标签的节点,并且新增了一个节点data,布局文件如下:

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
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<!--定义所需捆绑的变量,name变量名,type变量类型-->
<variable
name="testNormalDto"
type="com.jessie.databinding.study.dto.TestNormalDto" />
</data>


<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{testNormalDto.test1}" />

<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{testNormalDto.test2}" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{testNormalDto.test3}" />
</LinearLayout>
</layout>

吐槽一下,android studio的XML支持并不是很好,输入变量很多时候没有智能提示。这里我们引用了testNormalDto这个数据集,并将这个数据集的属性与具体的ui控件做了捆绑,在定义变量的同时,框架会自动为我们生成testNormalDto这个数据集的set方法。当我们到Activity中刷新这个数据集时,对应捆绑的控件也会跟着刷新。具体的代码如下:

1
2
3
4
ActivityMainBinding binding = DataBindingUtil.setContentView(
this, R.layout.activity_main);
TestNormalDto testNormalDto = new TestNormalDto("fei", "Liang" ,"sss");
binding.setTestNormalDto(testNormalDto);

ActivityMainBinding是DataBinding这个框架自动生成的,初始化时绑定当前的ui布局。创建一个model对象,注入到ActivityMainBinding的对象中,即可完成数据刷新操作。刷新model对象的同时刷新ui。MVVM 的 ViewModel 需要把数据(Model)与 UI(View) 进行绑定,data 节点就是连接它们之间的一个桥梁。

variable类型

variable同样支持基本数据类型,比如String,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<!--定义所需捆绑的变量,name变量名,type变量类型-->
<data>
<variable
name="myStr"
type="String"/>

</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{myStr}"/>

</LinearLayout>
</layout>

框架也会自动为我们生成set方法,刷新数据集的同时刷新控件显示的数据。

1
2
3
ActivityMainBinding binding = DataBindingUtil.setContentView(
this, R.layout.activity_main);
binding.setMyStr("测试字符串变量");

Integer, Float,Double也同样支持,不过这里不能直接显示在控件中,要转String再显示,DataBinding的数据绑定支持表达式,我们可以先定义一个工具类。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyStringUtils {
public static String convert(final Double myDouble) {
return myDouble+"";
}

public static String convert(final Integer myInteger) {
return myInteger+"";
}

public static String convert(final Float myFloat) {
return myFloat+"";
}

接下来是在XML中定义变量并绑定,同样,框架会自动为我们生成set方法,这里我们在绑定的时候调用这个工具类转String再显示到控件中,注意调用工具类需要先import工具类到xml中,我们需要在<data>标签中添加<import>标签。代码如下:

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
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<!--定义所需捆绑的变量,name变量名,type变量类型-->
<data>
<import type="com.jessie.databinding.study.dto.MyStringUtils"/>

<variable
name="myInt"
type="Integer"/>
<variable
name="myDouble"
type="Double"/>
<variable
name="myFloat"
type="Float"/>
</data>


<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{MyStringUtils.convert(myInt)}"/>


<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{MyStringUtils.convert(myDouble)}"/>


<TextView
android:id="@+id/textView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{MyStringUtils.convert(myFloat)}"/>
</LinearLayout>
</layout>

在Activity中刷新数据集,同时刷新控件显示数据

1
2
3
4
5
ActivityMainBinding binding = DataBindingUtil.setContentView(
this, R.layout.activity_main);
binding.setMyFloat(12.3f);
binding.setMyDouble(12.4);
binding.setMyInt(10);

自定义Binding的名称

除了使用自动生成的Binding名ActivityMainBinding以外,还可以在data标签中添加class属性

1
2
<data class="CustomBinding">
</data>

类型别名

如果此时项目根目录下也存在一个TestNormalDto,这时存在两个TestNormalDto,XML中需要引入这两个dto做数据捆绑,这时data 节点了导入了两个同名的类:

1
2
3
<import type="com.jessie.databinding.study.dto.TestNormalDto" />
<import type="com.jessie.databinding.study.TestNormalDto" />
<variable name="testNormalDto" type="TestNormalDto" />

import 还有一个 alias 属性,可解决同名类问题

1
2
3
<import type="com.jessie.databinding.study.dto.TestNormalDto" />
<import type="com.jessie.databinding.study.TestNormalDto" alias="NormalDto" />
<variable name="normalDto" type="NormalDto" />

非空判断

感觉类似java的三目运算符

1
android:text="@{user.displayName ?? user.lastName}"

这个等价于

1
android:text="@{user.displayName != null ? user.displayName : user.lastName}"

设置属性

如果TestNormalDto中添加一个属性isNormal,当该属性为true时,控件可见否则控件不可见,我们可以直接把 Java 中定义的属性值赋值给 xml 属性。

1
2
3
4
5
<TextView
android:text="@{testNormalDto.test1}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{testNormalDto.isNormal ? View.VISIBLE : View.GONE}"/>

使用资源

padding的大小通过屏幕尺寸自适应,大尺寸使用dimen资源中的largePadding,否则使用smallPadding。字符串引用也可使用占位符传参的形式。

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
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data >
<variable
name="large"
type="boolean" />
<variable
name="goodsName"
type="String" />

<variable
name="price"
type="double" />

</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@{large? (int)@dimen/largePadding : (int)@dimen/smallPadding}"
android:text="@{@string/nameFormat(goodsName,price)}"
/>
</LinearLayout>
</layout>

<!--string.xml-->
<resources>
<string name="nameFormat">%1$s的价格是%2$.2f</string>
</resources>

同样在Activity中刷新数据集

1
2
3
4
ResourceBinding binding = DataBindingUtil.setContentView(
this, R.layout.activity_main);
binding.setGoodsName("T-shirt");
binding.setPrice(15);

context引用

context是dataBinding通过variable标签默认帮我们定义好了,该context的值就是从当前布局文件中的根节点view的getContext()方法获取的.所以我们可以在布局中直接引用context

如果我们自己通过variable标签定义了一个name为context的变量,那么会覆盖掉系统提供的context。

ViewStubs的使用

XML布局中添加

1
2
3
4
5
<ViewStub
android:id="@+id/view_stub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout="@layout/inflate_content" />

inflate_content.xml代码与activity的布局文件大致相似,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data class="ViewStubBinding">
<variable
name="viewStubTag"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{viewStubTag}" />

</LinearLayout>
</layout>

在activity中inflate布局,并且增加inflate监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
binding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
ViewStubBinding binding = DataBindingUtil.bind(inflated);
binding.setViewStubTag("这是inflate的布局");
}
});

inflateViewStub(binding);
public void inflateViewStub(ActivityMainBinding binding) {
if (!binding.viewStub.isInflated()) {
binding.viewStub.getViewStub().inflate();
}
}

include的使用

XML布局中添加

1
2
3
4
5
6
7
8
<!--添加variable标签,定义变量-->
<variable
name="includeTag"
type="String"/>

<include
layout="@layout/include_content"
bind:includeTag="@{includeTag}" />

include_content布局中接收由activity布局传过来的数据代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data >
<variable
name="includeTag"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{includeTag}" />

</LinearLayout>
</layout>

在activity中加一句代码即可传入数据

1
binding.setIncludeTag("这是include的内容");

@BindingConversion与@BindingAdapter

XML布局中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--添加variable标签,定义变量-->
<variable
name="isNormal"
type="boolean" />

<variable
name="height"
type="float" />

<variable
name="time"
type="java.util.Date" />

<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@{isNormal ? @color/colorPrimary : @color/colorAccent}"
android:gravity="center"
bind:custom="@{height}" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{time}"/>
  • @BindingAdapter标签的用法:

在activity中添加绑定代码,bind:custom这个是我们自定义的属性,databinding会去查找自定义的setCustom的方法,需要添加注解@BindingAdapter,第一个参数必须是我们需要刷新的控件类型,第二个参数就是传入到我们自定义属性中的数据,这里我们传入的是float类型的高度值,我们可以根据这个高度值动态设置控件的高度。

  • @BindingConversion标签的用法:

在activity中添加绑定代码,android:text属性的目标类型是String,但是这里我们定义的是一个Date变量,传入后不能直接使用,所以我们需要在显示之前做一次转化,就像服务器给我们int型的数据,但是我们显示时,往往需要根据状态值显示string类型。添加注解@BindingConversion,传入我们定义的类型Date,转化为可显示的String类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@BindingConversion
public static String convertDate(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
if(date!=null){
return sdf.format(date);
}else{
return "";
}

}

@BindingAdapter("custom")
public static void setCustom(TextView textView, float height) {
ViewGroup.LayoutParams params = textView.getLayoutParams();
params.height = (int) height;
textView.setLayoutParams(params);
}

最后在activity中刷新数据

1
2
3
binding.setIsNormal(false);
binding.setHeight(400);
binding.setTime(new Date());

Recyclerview的动态绑定

只需获得item布局对应的binding对象,然后刷新数据即可,感觉与ButterKnife的使用方法类似。

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
public class ListAdapter extends RecyclerView.Adapter<ListAdapter.ListHolder> {
private static final int BEANS_COUNT = 10;

@NonNull
private List<Bean> beans;

public ListAdapter() {
beans = new ArrayList<>(10);
for (int i = 0; i < BEANS_COUNT; i ++) {
Bean bean = new Bean("测试数据"+i,i);
beans.add(bean);
}
}

public static class ListHolder extends RecyclerView.ViewHolder {
private BeanItemBinding binding;

public ListHolder(View itemView) {
super(itemView);
binding = DataBindingUtil.bind(itemView);
}

public void bind(@NonNull Bean bean) {
binding.setBean(bean);
}
}

@Override
public ListHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View itemView = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.bean_item, viewGroup, false);
return new ListHolder(itemView);
}

@Override
public void onBindViewHolder(ListHolder holder, int position) {
holder.bind(beans.get(position));
}

@Override
public int getItemCount() {
return beans.size();
}
}

Bean类的代码如下:

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 Bean {
private String name;
private int position;

public Bean(String name, int position) {
this.name = name;
this.position = position;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getPosition() {
return position;
}

public void setPosition(int position) {
this.position = position;
}
}

item_bean.xml中定义bean对象,并将属性与控件捆绑的代码如下:

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
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">


<data class="BeanItemBinding">
<import type="com.jessie.databinding.study.dto.MyStringUtils"></import>
<variable
name="bean"
type="com.jessie.databinding.study.dto.Bean">

</variable>
</data>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/textName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="@{bean.name}"/>

<TextView
android:id="@+id/textPosition"
android:layout_alignParentRight="true"
android:layout_marginRight="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{MyStringUtils.convert(bean.position)}"/>

</RelativeLayout>
</layout>

最后在activity中初始化recyclerview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RecyclerviewActivity  extends AppCompatActivity {
private RecyclerView recyclerView;

private ListAdapter adapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recyclerview);
recyclerView= (RecyclerView) this.findViewById(R.id.recyclerview);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter=new ListAdapter();

recyclerView.setAdapter(adapter);
}
}

Observable Binding配合实现双向绑定

Observable Binding有三种实现方式:

  1. 继承BaseObservable类
  2. 实现Observable接口
  3. 使用Observable封装的响应式对象

Observable Binding具体表现主要是在数据源发生改变时,自动通知view刷新数据。

  • BaseObservable是双向绑定的基础
  • 结合双向绑定表达式@={},可以做到当view内容变化时,自动通知数据源更新。

继承BaseObservable类实现

我们需要在DTO类继承BaseObservable类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ObservableBean extends BaseObservable{
private String test1;

@Bindable
public String getTest1() {
return test1;
}

public void setTest1(String test1) {
this.test1 = test1;
notifyPropertyChanged(BR.test1);
}
}

与以往不同的是,需要用@Bindable注解get方法,在set方法最后一行添加notifyPropertyChanged方法,通知数据刷新。

在xml布局文件中对相应控件进行数据绑定,代码如下:

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
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data >
<variable
name="observableBean"
type="com.jessie.databinding.study.dto.ObservableBean">
</variable>
</data>
<LinearLayout
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textObservable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{observableBean.test1}"/>

<EditText
android:id="@+id/editObservable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={observableBean.test1}"/>
</LinearLayout>

</layout>

注意,此时EditText的text赋值为@={xxx}就是在调用双向绑定,根据我们当前输入的字符串去刷新数据源,然后数据源再刷新对应的控件,我们可以在TextView中绑定相同的字段查看数据源中的字段是否同步更新。这里有个死循环的问题,需要对旧数据和新数据作比较,如果相同则是会引起死循环的重复刷新,是需要过滤掉的,不过EditText作为系统组件,databinding的框架中已经解决了。

在activity中刷新数据,跟以前没有区别,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
observableBean=new ObservableBean();
observableBean.setTest1("测试数据1");

ActivityObervableBinding binding = DataBindingUtil.setContentView(
this, R.layout.activity_obervable);
binding.setObservableBean(observableBean);
EditText editObservable=findViewById(R.id.editObservable);
editObservable.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

}

@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

}

@Override
public void afterTextChanged(Editable editable) {
Log.d(TAG, "afterTextChanged: "+observableBean.getTest1());
}
);

我们也可以通过EditText的监听去打印数据源,效果图和截图如下:

EditText中输入的效果图:
输入效果图

log打印出的截图:
输入效果图

这里可能会出现一个问题,我们的实体类已经继承了其他父类,不能再继承BaseObservable。第二种实现方式可以解决这个问题

实现Observable接口

只有DTO类需要修改,代码如下:

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 ObservableImpBean implements Observable {

private PropertyChangeRegistry registry =new PropertyChangeRegistry();
private String test1;

@Bindable
public String getTest1() {
return test1;
}

public void setTest1(String test1) {
this.test1 = test1;
registry.notifyChange(this, BR.test1);
}

@Override
public void addOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
registry.add(callback);
}

@Override
public void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
registry.remove(callback);
}
}

我们需要先实例化PropertyChangeRegistry对象,实现Observable接口必须实现addOnPropertyChangedCallback方法和removeOnPropertyChangedCallback方法,我们用PropertyChangeRegistry对象分别调用对应的add方法和remove方法传入callback即可,同样需要用@Bindable注解get方法,在set方法最后一行添加PropertyChangeRegistry对象调用notifyChange方法,通知数据刷新。事实上,BaseObservable也是实现了Observable这个接口。

最后一种常见情况,我们的实体类的字段很多很多,要是每个get/set方法都对应加上@Bindable注解和notifyPropertyChanged()方法,显得很麻烦,那么第三个方式可以解决这个问题

使用Observable封装的响应式对象

我们需要修改DTO类,代码如下:

1
2
3
public class ObservableObject {
public final ObservableField<String> test1 = new ObservableField<>();
}

这里用到了DataBinding提供的响应式对象,看起来是不是精简许多了呢?

基本类型的数据结构提的包装类

  • ObservableBoolean
  • ObservableByte
  • ObservableChar
  • ObservableDouble
  • ObservableFloat
  • ObservableInt
  • ObservableLong
  • ObservableShort

针对集合提供的包装类

  • ObservableArrayList
  • ObservableArrayMap

针对实现了Parcelable接口的对象提供的包装类

  • ObservableParcelable

针对其他类型提供的包装类

  • ObservableField。最典型的:ObservableField

接下来是在xml中绑定对应的控件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--添加variable标签,定义变量-->
<variable
name="observableObject"
type="com.jessie.databinding.study.dto.ObservableObject">
</variable>

<TextView
android:id="@+id/textObservable2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{observableObject.test1.get()}"/>

<EditText
android:id="@+id/editObservable2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={observableObject.test1.get()}"/>

我们可以看到包装类的set,get方法略有不同。
在activity中刷新数据:

1
2
3
observableObject=new ObservableObject();
observableObject.beanField.set("测试响应式对象");
binding.setObservableObject(observableObject);

最后注意一点,并不是只靠BaseObservable加上双向绑定表达式@={}就可以实现双向绑定。实际上在使用双向绑定时,还得依靠各个注解的帮助,监听view内容的变化是BaseObservable做不到的,需要依赖指定的注解才能把view内容的变化通知出去,然后BaseObservable收到这些通知后触发 notifyPropertyChanged(),改变数据源以及界面。而这些对于android自带的组件DataBinding已经处理好了,所以对于自定义的控件需要自己通过这些注解实现双向绑定。

MVPVM实战

自从google推出databinding后,View Interface的部分功能可以转移到ViewModel中去,进一步降低View层的臃肿。结合已经比较流行的MVP架构,衍生出一种新的架构,叫做MVPVM。MVPVM=Model+View+Presenter+ViewModel

除ViewModel层外,其余与MVP大致相同,各个分层的主要职责:

View层:实现View Interface,对外提供UI操作的方法,比如界面显示刷新,弹窗,消息提示。

ViewModel层:以databinding为基础,对外提供控制xml界面的方法。

Presenter层:实现Presenter Interface,调用model层处理业务逻辑并显示到View层上。

Model层:处理业务逻辑,如数据请求,缓存处理,数据库处理。

前一篇在介绍Dagger的时候写过登录功能,这里就登录功能以MVPVM的架构再写一个小例子。

dto为响应式对象

1
2
3
4
5
6
7

public class User extends BaseObservable {
public ObservableField<String> username=new ObservableField<>();//用户名
public ObservableField<String> password=new ObservableField<>();//密码
public ObservableInt usernamePass=new ObservableInt();//用户名校验状态
public ObservableInt passwordPass=new ObservableInt();//密码校验状态
}

M层没有什么变化

M层接口

1
2
3
4
5
public interface ILoginModel {
public boolean login(String username, String password);
public boolean checkUserName(String username);
public boolean checkPassword(String password);
}

M层实现

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
public class LoginModel implements ILoginModel {

// @Inject
public LoginModel(){

}
@Override
public boolean login(String username,String password) {
if("admin".equals(username)&&"123456".equals(password)){
return true;
}else{
return false;
}
}

@Override
public boolean checkUserName(String username) {
if("admin".equals(username)){
return true;
}else{
return false;
}
}

@Override
public boolean checkPassword(String password) {
if("123456".equals(password)){
return true;
}else{
return false;
}
}
}

P层需要传入V层接口,初始化响应式对象,并提供获取接口,响应式对象必须在V层绑定。

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
public class LoginPresenter {
private ILoginModel iLoginModel;
private ILoginView iLoginView;
private User user;
public LoginPresenter(ILoginView iLoginView){
iLoginModel = new LoginModel();
//初始化响应式对象
user=new User();
this.iLoginView=iLoginView;
}

public User getUser(){
return user;
}
public void checkPassword(){
//用户名输入框已输入
if(!TextUtils.isEmpty(user.password.get())){
if (iLoginModel.checkPassword(user.password.get())) {
user.passwordPass.set(1);
}else{
user.passwordPass.set(2);
}
}else{
user.passwordPass.set(0);
}
}

public void checkUserName(){
//密码输入框已输入
if(!TextUtils.isEmpty(user.username.get())){
if(iLoginModel.checkUserName(user.username.get())){
user.usernamePass.set(1);
}else{
user.usernamePass.set(2);
}

}else{
user.usernamePass.set(0);
}

}

public void login(){
if(iLoginModel.login(user.username.get(),user.password.get())){
iLoginView.onLoginResult(true);
}else{
iLoginView.onLoginResult(false);
}
}
}

V层接口,代码如下:

1
2
3
public interface ILoginView {
public void onLoginResult(boolean result);
}

V层实现代码如下:

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
public class LoginActivity extends AppCompatActivity implements ILoginView{

private static final String TAG = "LoginActivity";
private LoginPresenter loginPresenter;
EditText et_username,et_password;
TextInputLayout usernameInput;
TextInputLayout passwordInput;
User user;
ActivityLoginBinding activityLoginBinding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityLoginBinding= DataBindingUtil.setContentView(this,R.layout.activity_login);

loginPresenter=new LoginPresenter(this);
user=loginPresenter.getUser();
//初始化binding
activityLoginBinding.setUser(user);
et_username= (EditText) findViewById(R.id.et_username);
et_password= (EditText) findViewById(R.id.et_password);
usernameInput = (TextInputLayout) findViewById(R.id.usernameInput);
passwordInput = (TextInputLayout) findViewById(R.id.passwordInput);
et_username.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

}

@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

}

@Override
public void afterTextChanged(Editable editable) {
loginPresenter.checkUserName();
}
});

et_password.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

}

@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

}

@Override
public void afterTextChanged(Editable editable) {
Log.d(TAG, "afterTextChanged: "+user.password.get());
loginPresenter.checkPassword();
}
});
}


//登录按钮点击回调
public void onLoginClick(View view) {
hideKeyboard();
loginPresenter.login();
}

public void hideKeyboard(){
View view = getCurrentFocus();
if (view != null) {
((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)).
hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}

@BindingAdapter("onUsernameChange")
public static void setOnUsernameChange(TextInputLayout usernameInput, int result) {
if(result==0 || result==1){
usernameInput.setErrorEnabled(false);
}
else{
usernameInput.setError("用户名无效!");
}
}


@BindingAdapter("onPasswordChange")
public static void setOnPasswordChange(TextInputLayout passwordInput, int result) {
if(result==0 || result==1){
passwordInput.setErrorEnabled(false);
}else{
passwordInput.setError("密码无效!");

}
}

@Override
public void onLoginResult(boolean result) {
if(result){
Toast.makeText(this,"登录成功!",Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(this,"登录失败!",Toast.LENGTH_SHORT).show();
}
}

}

此处EditText使用了databinding的双向绑定,在输入用户名和密码的同时,刷新数据源。P层调用M层校验用户名密码状态,并结果返回到P层。TextInputLayout使用了@BindingAdapter注解绑定自定义属性,setOnUsernameChange方法绑定onUsernameChange属性,传入目标view和自定义属性的值,根据用户名的校验状态,设置对应TextInputLayout的错误提示。同样,setOnPasswordChange方法绑定onPasswordChange属性,传入目标view和自定义属性的值,根据密码的校验状态,设置对应TextInputLayout的错误提示。所以V层接口只需要提供登录成功后的提示接口。

LoginActivity的XML布局代码如下:

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
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">

<data>

<variable
name="user"
type="com.jessie.databinding.study.mvpvm.dto.User">

</variable>

</data>

<android.support.constraint.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.design.widget.TextInputLayout
android:id="@+id/usernameInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="52dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
bind:onUsernameChange="@{user.usernamePass}"
app:errorTextAppearance="@style/Theme.AppCompat"
app:layout_constraintBottom_toTopOf="@+id/passwordInput"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0"
tools:layout_constraintLeft_creator="1"
tools:layout_constraintRight_creator="1">

<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Username"
android:text="@={user.username}"
android:inputType="textEmailAddress"
tools:layout_editor_absoluteX="8dp"
tools:layout_editor_absoluteY="28dp" />

</android.support.design.widget.TextInputLayout>


<android.support.design.widget.TextInputLayout
android:id="@+id/passwordInput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/usernameInput"
android:layout_marginBottom="257dp"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
bind:onPasswordChange="@{user.passwordPass}"
app:errorTextAppearance="@style/Theme.AppCompat"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
tools:layout_constraintBottom_creator="1"
tools:layout_constraintLeft_creator="1"
tools:layout_constraintRight_creator="1">


<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:text="@={user.password}"
android:inputType="textPassword"
tools:layout_editor_absoluteY="74dp" />
</android.support.design.widget.TextInputLayout>

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="139dp"
android:onClick="onLoginClick"
android:text="登录"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
tools:layout_constraintBottom_creator="1"
tools:layout_constraintLeft_creator="1"
tools:layout_constraintRight_creator="1" />

<android.support.constraint.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="192dp" />


</android.support.constraint.ConstraintLayout>
</layout>

databinding的总结:

优点:解决界面与业务分离问题,简化代码,可提高开发效率。

缺点:错误提示很难定位,XML中数据捆绑的代码编辑提示不够智能。

我已经把所有例子的完整代码打包上传,想查看demo源码的朋友,这里有传送门