上一篇:Android 天气APP(三)访问天气API与数据请求
MVP/MVVM框架搭建与使用
新版-------------------
在上一篇文章中通过OkHttp请求成功获取了城市的LocationID,而如果每一个请求我们就这样写一个无疑是很麻烦的事情,因此我们需要封装一下,而既然是新版的内容,我们封装的方式就会和原来不同,不再使用MVP模式,而采用MVVM模式。
一、创建依赖模块
为了方便你做迁移,我就新增一个library moudle,里面就做MVVM的一些封装和处理,以及网络框架的处理,鼠标右键点击你的工程,最后点击Module。
这里就直接命名为library。
点击Finish进行模块的创建。
这里子模块就创建成功了,下面我们让app模块去依赖子模块,打开app的build.gradle,在dependencies{}闭包中增加如下代码:
implementation project(path: ':library')
并且将之前的OkHttp依赖剪切,如下图所示:
再打开library的build.gradle,在dependencies{}闭包下粘贴刚才所剪切的代码,如下图所示:
点击Sync Now进行同步,完成之后你的MainActivity应该会报错,为什么报错呢?因为我们在子模块中依赖的OKHttp,就需要在子模块中使用OKHttp,app模块无法使用,不过你要是把implementation
改成api
,那么app模块中的报错就会消失了,api就是让你的主模块能够使用子模块所依赖的远程依赖库。
不过我们并不需要这么做,为什么?因为我们会将网络请求整体放在library模块,app模块不直接使用依赖库,因此我们可以先将MainActivity中的searchCity()
方法删除掉,调用方法的语句也删除、包括导包的语句也删除掉。下面我们将使用OKHttp + Retrofit2 + RxJava2的方式来封装网络请求框架。
二、模块初始化
这里我们先在library模块的com.llw.library
包下创建一个ActivityManager类,用于管理所有的Activity,代码如下所示:
public class ActivityManager {
//保存所有创建的Activity
private final List<Activity> allActivities = new ArrayList<>();
/**
* 添加Activity到管理器
*
* @param activity activity
*/
public void addActivity(Activity activity) {
if (activity != null) {
allActivities.add(activity);
}
}
/**
* 从管理器移除Activity
*
* @param activity activity
*/
public void removeActivity(Activity activity) {
if (activity != null) {
allActivities.remove(activity);
}
}
/**
* 关闭所有Activity
*/
public void finishAll() {
for (Activity activity : allActivities) {
activity.finish();
}
}
public Activity getTaskTop() {
return allActivities.get(allActivities.size() - 1);
}
}
然后再创建一个BaseApplication类,同样在com.llw.library
包下,代码如下所示:
public class BaseApplication extends Application {
private static ActivityManager activityManager;
@SuppressLint("StaticFieldLeak")
private static BaseApplication application;
@SuppressLint("StaticFieldLeak")
private static Context context;
@Override
public void onCreate() {
super.onCreate();
//声明Activity管理
activityManager = new ActivityManager();
context = getApplicationContext();
application = this;
}
public static ActivityManager getActivityManager() {
return activityManager;
}
//内容提供器
public static Context getContext() {
return context;
}
public static BaseApplication getApplication() {
return application;
}
}
这两个类的作用就是为了管理工程的,修改app模块的WeatherApp,让它继承自刚才写的BaseApplication。
好了,这里先告一段落,我们写网络框架。
三、搭建网络框架
首先在library的build.gradle中的dependencies{}闭包下添加如下所示代码:
//OKHttp
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
//retrofit2
api 'com.squareup.retrofit2:retrofit:2.9.0'
//这里用api 是为了让其他模块也可以使用gson
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
//日志拦截器
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
//rxjava
api 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.12'
添加位置如下图所示:
Sync Now同步一下,下面在com.llw.library
包下创建一个network包,这里面我们就写网络请求的相关代码。在network包下创建一个接口INetworkRequiredInfo
,在里面写一些回调的方法,用于获取App的版本名、版本号、运行状态、全局上下文参数,里面代码如下:
public interface INetworkRequiredInfo {
/**
* 获取App版本名
*/
String getAppVersionName();
/**
* 获取App版本号
*/
String getAppVersionCode();
/**
* 判断是否为Debug模式
*/
boolean isDebug();
/**
* 获取全局上下文参数
*/
Application getApplicationContext();
}
因为考虑到会使用很多的接口API,它们的地址都不一样,所以我们创建一个枚举类ApiType
,在network包下创建,代码如下:
public enum ApiType {
SEARCH
}
目前只有一个搜索,后面如果有新增就往里面加,然后创建一个基础返回类,在network包下新建一个BaseResponse类,代码如下:
public class BaseResponse {
/**
* 结果码
*/
@SerializedName("res_code")
@Expose
public Integer responseCode;
/**
* 返回的错误信息
*/
@SerializedName("res_error")
@Expose
public String responseError;
}
下面在network包下新建一个observer包,里面创建一个抽象类BaseObserver,实现rxjava的Observer接口,代码如下:
public abstract class BaseObserver<T> implements Observer<T> {
//开始
@Override
public void onSubscribe(Disposable d) {
}
//继续
@Override
public void onNext(T t) {
onSuccess(t);
}
//异常
@Override
public void onError(Throwable e) {
onFailure(e);
}
//完成
@Override
public void onComplete() {
}
//成功
public abstract void onSuccess(T t);
//失败
public abstract void onFailure(Throwable e);
}
这里你需要注意一点就是导包别导错。
因为还有其他的库里面也有Observer。
在网络数据交互的时候有请求和返回,那么在两个过程中我们可以获取一些信息,就需要拦截器,在network包下新建一个interceptor包,interceptor包下新建RequestInterceptor类,里面的代码如下:
public class RequestInterceptor implements Interceptor {
/**
* 网络请求信息
*/
private final INetworkRequiredInfo iNetworkRequiredInfo;
public RequestInterceptor(INetworkRequiredInfo iNetworkRequiredInfo) {
this.iNetworkRequiredInfo = iNetworkRequiredInfo;
}
/**
* 拦截
*/
@Override
public Response intercept(Chain chain) throws IOException {
//构建器
Request.Builder builder = chain.request().newBuilder();
//添加使用环境
builder.addHeader("os", "android");
//添加版本号
builder.addHeader("appVersionCode", this.iNetworkRequiredInfo.getAppVersionCode());
//添加版本名
builder.addHeader("appVersionName", this.iNetworkRequiredInfo.getAppVersionName());
//添加日期时间
builder.addHeader("datetime", getNowDateTime());
//返回
return chain.proceed(builder.build());
}
public static String getNowDateTime() {
@SuppressLint("SimpleDateFormat") SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(new Date());
}
}
然后我们在interceptor包下新建一个返回拦截器ResponseInterceptor ,代码如下:
public class ResponseInterceptor implements Interceptor {
private static final String TAG = ResponseInterceptor.class.getSimpleName();
/**
* 拦截
*/
@Override
public Response intercept(Chain chain) throws IOException {
long requestTime = System.currentTimeMillis();
Response response = chain.proceed(chain.request());
Log.i(TAG, "requestSpendTime=" + (System.currentTimeMillis() - requestTime) + "ms");
return response;
}
}
在网络请求中可能还会有一些错误状态,因此也需要做处理,在network包下新建一个errorhandler包,errorhandler包下创建ExceptionHandle类,代码如下:
public class ExceptionHandle {
//未授权
private static final int UNAUTHORIZED = 401;
//禁止的
private static final int FORBIDDEN = 403;
//未找到
private static final int NOT_FOUND = 404;
//请求超时
private static final int REQUEST_TIMEOUT = 408;
//内部服务器错误
private static final int INTERNAL_SERVER_ERROR = 500;
//错误网关
private static final int BAD_GATEWAY = 502;
//暂停服务
private static final int SERVICE_UNAVAILABLE = 503;
//网关超时
private static final int GATEWAY_TIMEOUT = 504;
/**
* 处理异常
* @param throwable
* @return
*/
public static ResponseThrowable handleException(Throwable throwable) {
//返回时抛出异常
ResponseThrowable responseThrowable;
if (throwable instanceof HttpException) {
HttpException httpException = (HttpException) throwable;
responseThrowable = new ResponseThrowable(throwable, ERROR.HTTP_ERROR);
switch (httpException.code()) {
case UNAUTHORIZED:
case FORBIDDEN:
case NOT_FOUND:
case REQUEST_TIMEOUT:
case GATEWAY_TIMEOUT:
case INTERNAL_SERVER_ERROR:
case BAD_GATEWAY:
case SERVICE_UNAVAILABLE:
default:
responseThrowable.message = "网络错误";
break;
}
return responseThrowable;
} else if (throwable instanceof ServerException) {
//服务器异常
ServerException resultException = (ServerException) throwable;
responseThrowable = new ResponseThrowable(resultException, resultException.code);
responseThrowable.message = resultException.message;
return responseThrowable;
} else if (throwable instanceof JsonParseException
|| throwable instanceof JSONException
|| throwable instanceof ParseException) {
responseThrowable = new ResponseThrowable(throwable, ERROR.PARSE_ERROR);
responseThrowable.message = "解析错误";
return responseThrowable;
} else if (throwable instanceof ConnectException) {
responseThrowable = new ResponseThrowable(throwable, ERROR.NETWORK_ERROR);
responseThrowable.message = "连接失败";
return responseThrowable;
} else if (throwable instanceof javax.net.ssl.SSLHandshakeException) {
responseThrowable = new ResponseThrowable(throwable, ERROR.SSL_ERROR);
responseThrowable.message = "证书验证失败";
return responseThrowable;
} else if (throwable instanceof ConnectTimeoutException){
responseThrowable = new ResponseThrowable(throwable, ERROR.TIMEOUT_ERROR);
responseThrowable.message = "连接超时";
return responseThrowable;
} else if (throwable instanceof java.net.SocketTimeoutException) {
responseThrowable = new ResponseThrowable(throwable, ERROR.TIMEOUT_ERROR);
responseThrowable.message = "连接超时";
return responseThrowable;
}
else {
responseThrowable = new ResponseThrowable(throwable, ERROR.UNKNOWN);
responseThrowable.message = "未知错误";
return responseThrowable;
}
}
/**
* 约定异常
*/
public static class ERROR {
/**
* 未知错误
*/
public static final int UNKNOWN = 1000;
/**
* 解析错误
*/
public static final int PARSE_ERROR = 1001;
/**
* 网络错误
*/
public static final int NETWORK_ERROR = 1002;
/**
* 协议出错
*/
public static final int HTTP_ERROR = 1003;
/**
* 证书出错
*/
public static final int SSL_ERROR = 1005;
/**
* 连接超时
*/
public static final int TIMEOUT_ERROR = 1006;
}
public static class ResponseThrowable extends Exception {
public int code;
public String message;
public ResponseThrowable(Throwable throwable, int code) {
super(throwable);
this.code = code;
}
}
public static class ServerException extends RuntimeException {
public int code;
public String message;
}
}
errorhandler包下创建HttpErrorHandler类,代码如下:
public class HttpErrorHandler<T> implements Function<Throwable, Observable<T>> {
/**
* 处理以下两类网络错误:
* 1、http请求相关的错误,例如:404,403,socket timeout等等;
* 2、应用数据的错误会抛RuntimeException,最后也会走到这个函数来统一处理;
*/
@Override
public Observable<T> apply(Throwable throwable) throws Exception {
//通过这个异常处理,得到用户可以知道的原因
return Observable.error(ExceptionHandle.handleException(throwable));
}
}
最后我们在network包下新建一个NetworkApi类,里面就是核心代码了,代码如下:
public class NetworkApi {
//获取APP运行状态及版本信息,用于日志打印
private static INetworkRequiredInfo iNetworkRequiredInfo;
//OkHttp客户端
private static OkHttpClient okHttpClient;
//retrofitHashMap
private static final HashMap<String, Retrofit> retrofitHashMap = new HashMap<>();
//API访问地址
private static String mBaseUrl;
/**
* 初始化
*/
public static void init(INetworkRequiredInfo networkRequiredInfo) {
iNetworkRequiredInfo = networkRequiredInfo;
}
/**
* 创建serviceClass的实例
*/
public static <T> T createService(Class<T> serviceClass, ApiType apiType) {
getBaseUrl(apiType);
return getRetrofit(serviceClass).create(serviceClass);
}
/**
* 修改访问地址
*
* @param apiType api类型
*/
private static void getBaseUrl(ApiType apiType) {
switch (apiType) {
case SEARCH:
mBaseUrl = "https://geoapi.qweather.com";//和风天气搜索城市
break;
default:
break;
}
}
/**
* 配置OkHttp
*
* @return OkHttpClient
*/
private static OkHttpClient getOkHttpClient() {
//不为空则说明已经配置过了,直接返回即可。
if (okHttpClient == null) {
//OkHttp构建器
OkHttpClient.Builder builder = new OkHttpClient.Builder();
//设置缓存大小
int cacheSize = 100 * 1024 * 1024;
//设置OkHttp网络缓存
builder.cache(new Cache(iNetworkRequiredInfo.getApplicationContext().getCacheDir(), cacheSize));
//设置网络请求超时时长,这里设置为10s
builder.connectTimeout(10, TimeUnit.SECONDS);
builder.readTimeout(20, TimeUnit.SECONDS).build();
//添加请求拦截器,如果接口有请求头的话,可以放在这个拦截器里面
builder.addInterceptor(new RequestInterceptor(iNetworkRequiredInfo));
//添加返回拦截器,可用于查看接口的请求耗时,对于网络优化有帮助
builder.addInterceptor(new ResponseInterceptor());
//当程序在debug过程中则打印数据日志,方便调试用。
if (iNetworkRequiredInfo != null && iNetworkRequiredInfo.isDebug()) {
//iNetworkRequiredInfo不为空且处于debug状态下则初始化日志拦截器
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
//设置要打印日志的内容等级,BODY为主要内容,还有BASIC、HEADERS、NONE。
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
//将拦截器添加到OkHttp构建器中
builder.addInterceptor(httpLoggingInterceptor);
}
//OkHttp配置完成
okHttpClient = builder.build();
}
return okHttpClient;
}
/**
* 配置Retrofit
*
* @param serviceClass 服务类
* @return Retrofit
*/
private static Retrofit getRetrofit(Class serviceClass) {
if (retrofitHashMap.get(mBaseUrl + serviceClass.getName()) != null) {
//刚才上面定义的Map中键是String,值是Retrofit,当键不为空时,必然有值,有值则直接返回。
return retrofitHashMap.get(mBaseUrl + serviceClass.getName());
}
//初始化Retrofit Retrofit是对OKHttp的封装,通常是对网络请求做处理,也可以处理返回数据。
//Retrofit构建器
Retrofit.Builder builder = new Retrofit.Builder();
//设置访问地址
builder.baseUrl(mBaseUrl);
//设置OkHttp客户端,传入上面写好的方法即可获得配置后的OkHttp客户端。
builder.client(getOkHttpClient());
//设置数据解析器 会自动把请求返回的结果(json字符串)通过Gson转化工厂自动转化成与其结构相符的实体Bean
builder.addConverterFactory(GsonConverterFactory.create());
//设置请求回调,使用RxJava 对网络返回进行处理
builder.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
//retrofit配置完成
Retrofit retrofit = builder.build();
//放入Map中
retrofitHashMap.put(mBaseUrl + serviceClass.getName(), retrofit);
//最后返回即可
return retrofit;
}
/**
* 配置RxJava 完成线程的切换,如果是Kotlin中完全可以直接使用协程
*
* @param observer 这个observer要注意不要使用lifecycle中的Observer
* @param <T> 泛型
* @return Observable
*/
public static <T> ObservableTransformer<T, T> applySchedulers(final Observer<T> observer) {
return new ObservableTransformer<T, T>() {
@Override
public ObservableSource<T> apply(Observable<T> upstream) {
Observable<T> observable = upstream
.subscribeOn(Schedulers.io())//线程订阅
.observeOn(AndroidSchedulers.mainThread())//观察Android主线程
.map(NetworkApi.<T>getAppErrorHandler())//判断有没有500的错误,有则进入getAppErrorHandler
.onErrorResumeNext(new HttpErrorHandler<T>());//判断有没有400的错误
//这里还少了对异常
//订阅观察者
observable.subscribe(observer);
return observable;
}
};
}
/**
* 错误码处理
*/
protected static <T> Function<T, T> getAppErrorHandler() {
return new Function<T, T>() {
@Override
public T apply(T response) throws Exception {
//当response返回出现500之类的错误时
if (response instanceof BaseResponse && ((BaseResponse) response).responseCode >= 500) {
//通过这个异常处理,得到用户可以知道的原因
ExceptionHandle.ServerException exception = new ExceptionHandle.ServerException();
exception.code = ((BaseResponse) response).responseCode;
exception.message = ((BaseResponse) response).responseError != null ? ((BaseResponse) response).responseError : "";
throw exception;
}
return response;
}
};
}
}
通过这个类,我们将刚才所写的拦截,错误处理等都写进去了。
这里network包下的所有内容,检查一下自己是否正确,通过是否有报错,有报错的话就很有可能是你导包导错了,看看我的源码就知道了。
四、基础封装
在前面我们用到了ViewBinding,那么对于ViewBinding我们可不可封装一下呢?当然是可以的,当前的library中还不能使用ViewBinding的,我们需要添加一个依赖,在library模块的build.gradle的dependencies{}下添加如下代码:
implementation 'androidx.databinding:viewbinding:7.3.1'
添加位置如下图所示:
在com.llw.library包下新建一个base包,里面创建一个BaseActivity类,代码如下:
public class BaseActivity extends AppCompatActivity {
protected AppCompatActivity mContext;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this;
BaseApplication.getActivityManager().addActivity(this);
}
protected void showMsg(CharSequence msg) {
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
}
protected void showLongMsg(CharSequence msg) {
Toast.makeText(mContext, msg, Toast.LENGTH_LONG).show();
}
/**
* 跳转页面
*
* @param clazz 目标页面
*/
protected void jumpActivity(final Class<?> clazz) {
startActivity(new Intent(mContext, clazz));
}
/**
* 跳转页面并关闭当前页面
*
* @param clazz 目标页面
*/
protected void jumpActivityFinish(final Class<?> clazz) {
startActivity(new Intent(mContext, clazz));
finish();
}
protected void back(Toolbar toolbar) {
toolbar.setNavigationOnClickListener(v -> onBackPressed());
}
protected void backAndFinish(Toolbar toolbar) {
toolbar.setNavigationOnClickListener(v -> finish());
}
/**
* 检查是有拥有某权限
*
* @param permission 权限名称
* @return true 有 false 没有
*/
protected boolean hasPermission(String permission) {
return checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
}
/**
* 退出应用程序
*/
protected void exitTheProgram() {
BaseApplication.getActivityManager().finishAll();
}
}
这个基类中,我们写了一些常用的方法,供继承这个类的子类去使用,然后是对ViewBinding的封装,在base包下创建BaseVBActivity类,代码如下:
public abstract class BaseVBActivity<VB extends ViewBinding> extends BaseActivity {
protected VB binding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
onRegister();
super.onCreate(savedInstanceState);
Type type = this.getClass().getGenericSuperclass();
if (type instanceof ParameterizedType) {
try {
Class<VB> clazz = (Class<VB>) ((ParameterizedType) type).getActualTypeArguments()[0];
//反射
Method method = clazz.getMethod("inflate", LayoutInflater.class);
binding = (VB) method.invoke(null, getLayoutInflater());
} catch (Exception e) {
e.printStackTrace();
}
setContentView(binding.getRoot());
}
initData();
}
protected void onRegister() {
}
protected abstract void initData();
}
这个类继承自BaseActivity,里面主要就是反射拿到具体的编译时类,然后设置内容视图,同时我将onRegister放了进来,这不是一个必须实现的方法,还记得在MainActivity中写的registerIntent()方法吗?和这个方法如出一辙,后面如果我们需要使用意图去出去,就可以在子类中直接重写父类的方法达到同样的效果。
然后在base包下再创建一个NetworkActivity类,代码如下:
public abstract class NetworkActivity<VB extends ViewBinding> extends BaseVBActivity<VB> {
@Override
public void initData() {
onCreate();
onObserveData();
}
protected abstract void onCreate();
protected abstract void onObserveData();
}
这是一个网络请求类,继承自BaseVBActivity,里面有两个抽象方法,onCreate()自然不用多说了,而onObserveData()就是在使用LiveData的时候有一个观察数据返回的地方,为此我写了一个抽象方法,这属于MVVM框架的一部分,但并不是那么严格,在这个类中我们实现了BaseVBActivity类的initData()抽象方法,那么如果有一个类继承自NetworkActivity,就不需要重复实现了,只需要实现onCreate()和onObserveData()即可。
最后我们在base包下新建一个BaseViewModel类,代码如下:
public class BaseViewModel extends ViewModel {
protected MutableLiveData<String> failed = new MutableLiveData<>();
}
这里我们继承自ViewModel ,然后写一个可变数据的LiveData,方便继承者直接使用这个failed,base包的内容就这些了,我们再把BaseApplication和ActivityManager放到这个包里面,这样看起来就很清楚明了。
下面到了使用的环节了。
五、使用
首先我们需要初始化网络框架,在com.llw.goodweather包下新建一个NetworkRequiredInfo类,代码如下:
public class NetworkRequiredInfo implements INetworkRequiredInfo {
private final Application application;
public NetworkRequiredInfo(Application application){
this.application = application;
}
/**
* 版本名
*/
@Override
public String getAppVersionName() {
return BuildConfig.VERSION_NAME;
}
/**
* 版本号
*/
@Override
public String getAppVersionCode() {
return String.valueOf(BuildConfig.VERSION_CODE);
}
/**
* 是否为debug
*/
@Override
public boolean isDebug() {
return BuildConfig.DEBUG;
}
/**
* 应用全局上下文
*/
@Override
public Application getApplicationContext() {
return application;
}
}
如果你发现你的BuildConfig爆红,那就编译一下就不会了,编译时会生成一个临时类,这里你不需要导包,导包反而错了,然后我们在WeatherApp中,对网络框架进行初始化,在onCreate()中新增如下代码:
NetworkApi.init(new NetworkRequiredInfo(this));
添加位置如下图所示:
在网络请求使用过程中我们会用到一些常量,因此在com.llw.goodweather包下新建一个Constant类,代码如下:
public class Constant {
/**
* 和风天气的KEY,请使用自己的
*/
public final static String API_KEY = "d4a619bfe3244190bfa84bb468c14316";
/**
* 和风天气接口请求成功状态码
*/
public static final String SUCCESS = "200";
/**
* 搜索类型:精准搜索
*/
public static final String EXACT = "exact";
/**
* 搜索类型:模糊搜索
*/
public static final String FUZZY = "fuzzy";
}
下面可以去写API接口了,在com.llw.goodweather包下新建ApiService接口,代码如下:
public interface ApiService {
/**
* 搜索城市 模糊搜索,国内范围 返回10条数据
*
* @param location 城市名
* @param mode exact 精准搜索 fuzzy 模糊搜索
* @return NewSearchCityResponse 搜索城市数据返回
*/
@GET("/v2/city/lookup?key=" + API_KEY + "&range=cn")
Observable<SearchCityResponse> searchCity(@Query("location") String location,
@Query("mode") String mode);
}
这里面用到的API_KEY就是刚才所定义的常量,你导包就可以了,类似下图这样。
在MVVM框架中,是Model + View + ViewModel的模式,Model我们之前已经写好了,就是SearchCityResponse,我们的搜索城市数据实体类,那么View表示视图,就是我们的Activity,而ViewModel就是负责连接View,所以在ViewModel中需要获取Model,拿到数据给到View,而如果直接在ViewModel中请求网络又比较臃肿,因此再拆分一下,在ViewModel使用Repository,作为数据处理的方式。
下面在com.llw.goodweather包下新建一个repository包,包下新建一个SearchCityRepository类,代码如下:
@SuppressLint("CheckResult")
public class SearchCityRepository {
private static final String TAG = SearchCityRepository.class.getSimpleName();
public void searchCity(MutableLiveData<SearchCityResponse> responseLiveData,
MutableLiveData<String> failed, String cityName, boolean isExact) {
NetworkApi.createService(ApiService.class, ApiType.SEARCH).searchCity(cityName, isExact ? Constant.EXACT : Constant.FUZZY)
.compose(NetworkApi.applySchedulers(new BaseObserver<>() {
@Override
public void onSuccess(SearchCityResponse searchCityResponse) {
if (searchCityResponse == null) {
failed.postValue("搜索城市数据为null,请检查城市名称是否正确。");
return;
}
//请求接口成功返回数据,失败返回状态码
if (Constant.SUCCESS.equals(searchCityResponse.getCode())) {
responseLiveData.postValue(searchCityResponse);
} else {
failed.postValue(searchCityResponse.getCode());
}
}
@Override
public void onFailure(Throwable e) {
Log.e(TAG, "onFailure: " + e.getMessage());
failed.postValue(e.getMessage());
}
}));
}
}
这里就是用到了网络框架,OKHttp做网络请求,Retrofit做接口封装和解析,RxJava做线程切换调度。拿到数据之后我们在通过LiveData进行发送。
下面我们就需要创建ViewModel了,在com.llw.goodweather包下新建viewmodel包,包下新建MainViewModel类,这里是对应MainActivity的,代码如下:
public class MainViewModel extends BaseViewModel {
public MutableLiveData<SearchCityResponse> searchCityResponseMutableLiveData = new MutableLiveData<>();
/**
* 搜索成功
* @param cityName 城市名称
* @param isExact 是否精准搜索
*/
public void searchCity(String cityName, boolean isExact) {
new SearchCityRepository().searchCity(searchCityResponseMutableLiveData, failed, cityName, isExact);
}
}
这里我们继承自BaseViewModel ,然后创建searchCityResponseMutableLiveData和一个searchCity,再去调用SearchCityRepository的searchCity()。
最后就到了到使用的地方了,我们需要改造一下之前的MainActivity,改造后代码如下:
public class MainActivity extends NetworkActivity<ActivityMainBinding> implements LocationCallback {
//权限数组
private final String[] permissions = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.WRITE_EXTERNAL_STORAGE};
//请求权限意图
private ActivityResultLauncher<String[]> requestPermissionIntent;
public LocationClient mLocationClient = null;
private final MyLocationListener myListener = new MyLocationListener();
private MainViewModel viewModel;
/**
* 注册意图
*/
@Override
public void onRegister() {
//请求权限意图
requestPermissionIntent = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
boolean fineLocation = Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION));
boolean writeStorage = Boolean.TRUE.equals(result.get(Manifest.permission.WRITE_EXTERNAL_STORAGE));
if (fineLocation && writeStorage) {
//权限已经获取到,开始定位
startLocation();
}
});
}
/**
* 初始化
*/
@Override
protected void onCreate() {
initLocation();
requestPermission();
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
}
/**
* 数据观察
*/
@Override
protected void onObserveData() {
if (viewModel != null) {
viewModel.searchCityResponseMutableLiveData.observe(this, searchCityResponse -> {
List<SearchCityResponse.LocationBean> location = searchCityResponse.getLocation();
if (location != null && location.size() > 0) {
String id = location.get(0).getId();
Log.d("TAG", "城市ID: " + id);
}
});
}
}
/**
* 请求权限
*/
private void requestPermission() {
//因为项目的最低版本API是23,所以肯定需要动态请求危险权限,只需要判断权限是否拥有即可
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
|| checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
//开始权限请求
requestPermissionIntent.launch(permissions);
return;
}
//开始定位
startLocation();
}
/**
* 初始化定位
*/
private void initLocation() {
try {
mLocationClient = new LocationClient(getApplicationContext());
} catch (Exception e) {
e.printStackTrace();
}
if (mLocationClient != null) {
myListener.setCallback(this);
//注册定位监听
mLocationClient.registerLocationListener(myListener);
LocationClientOption option = new LocationClientOption();
//如果开发者需要获得当前点的地址信息,此处必须为true
option.setIsNeedAddress(true);
//可选,设置是否需要最新版本的地址信息。默认不需要,即参数为false
option.setNeedNewVersionRgc(true);
//需将配置好的LocationClientOption对象,通过setLocOption方法传递给LocationClient对象使用
mLocationClient.setLocOption(option);
}
}
/**
* 开始定位
*/
private void startLocation() {
if (mLocationClient != null) {
mLocationClient.start();
}
}
/**
* 接收定位信息
*
* @param bdLocation 定位数据
*/
@Override
public void onReceiveLocation(BDLocation bdLocation) {
double latitude = bdLocation.getLatitude(); //获取纬度信息
double longitude = bdLocation.getLongitude(); //获取经度信息
float radius = bdLocation.getRadius(); //获取定位精度,默认值为0.0f
String coorType = bdLocation.getCoorType();
//获取经纬度坐标类型,以LocationClientOption中设置过的坐标类型为准
int errorCode = bdLocation.getLocType();//161 表示网络定位结果
//获取定位类型、定位错误返回码,具体信息可参照类参考中BDLocation类中的说明
String addr = bdLocation.getAddrStr(); //获取详细地址信息
String country = bdLocation.getCountry(); //获取国家
String province = bdLocation.getProvince(); //获取省份
String city = bdLocation.getCity(); //获取城市
String district = bdLocation.getDistrict(); //获取区县
String street = bdLocation.getStreet(); //获取街道信息
String locationDescribe = bdLocation.getLocationDescribe(); //获取位置描述信息
binding.tvAddressDetail.setText(addr);//设置文本显示
if (viewModel != null && district != null) {
//搜索城市
viewModel.searchCity(district);
} else {
Log.e("TAG", "district: " + district);
}
}
}
建议你不要直接复制粘贴,而是去了解这是怎么一回事,不然你遇到问题就是为什么,一点自己的思考都没有,别人能帮你一时,不能帮你一世。
用手机真机运行看看,要注意网络是否正常使用。
这里城市的ID就拿到了,下一篇文章就是通过城市ID获取天气数据并显示出来,最后看一下app模块的目录。
检查一下是否一致。
六、文章源码
欢迎 Star 和 Fork
第四篇文章源码地址:GoodWeather-New-4
旧版-------------------
4. MVP框架搭建
现在这样固然符合网络请求的标准,结果也得到了,但是这只是一个接口而已,我们用了这么多代码,那假如这个页面上还有好几个接口要请求访问,岂不是多出了很多的重复代码,这一点并不符合现在Android的现状,所以需要封装OKHttp,通过架构或者框架来完成这一步,前期虽然麻烦一些,但是你一旦用习惯了,就停不下来了,接下来我尽量用人话来讲述这个搭建过程。
为了让你有一个清晰的思路,这里创建一个模块,里面搭建MVP框架。
① 创建模块
鼠标右键你的项目名,选择Module
点击Finish
现在模块就创建完成了。
② 配置模块
接下来修改模块的build.gradle
代码如下:
apply plugin: 'com.android.library'
android {
compileSdkVersion 28
buildToolsVersion "28.0.3"
defaultConfig {
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//在模块中添加的依赖若想在项目中使用,则implementation改成api
//butterknife 绑定视图依赖BindView,告别findById,不过你还得安装一个butterknife插件才行
api 'com.jakewharton:butterknife:10.1.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.1.0'
//Google Material控件,以及迁移到AndroidX下一些控件的依赖
api 'com.google.android.material:material:1.0.0'
api 'androidx.lifecycle:lifecycle-extensions:2.1.0'
api 'androidx.annotation:annotation:1.1.0'
api 'androidx.legacy:legacy-support-v4:1.0.0'
//RecyclerView最好的适配器,让你的适配器一目了然,告别代码冗余
api 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30'
//图片加载框架
api 'com.github.bumptech.glide:glide:4.10.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
//权限请求框架
api 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
api 'io.reactivex.rxjava2:rxandroid:2.0.2'
api "io.reactivex.rxjava2:rxjava:2.0.0"
//状态栏
api 'com.readystatesoftware.systembartint:systembartint:1.0.3'
//支持okhttp
api 'com.squareup.okhttp3:okhttp:3.8.1'
api 'com.squareup.retrofit2:retrofit:2.4.0'
api 'com.squareup.retrofit2:converter-gson:2.4.0'
api 'com.squareup.okhttp3:logging-interceptor:3.4.1'
//阿里巴巴 FastJson
api 'com.alibaba:fastjson:1.2.57'
//下拉刷新框架
api 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0-alpha-14'
//没有使用特殊Header,可以不加这行
api 'com.scwang.smartrefresh:SmartRefreshHeader:1.1.0-alpha-14'
}
然后修改项目的build.gradle
implementation project(':mvplibrary')//引入模块 然后将项目里的依赖移动到模块的build.gradle里
然后Sync一下,如果没有出现什么问题就可以进行下一步了。
③ 创建Activity管理
在模块的com.llw.mvplibrary包下新建一个utils包,包下创建一个ActivityManagerl类,管理所有的Activity
代码如下:
package com.llw.mvplibrary.utils;
import android.app.Activity;
import java.util.ArrayList;
import java.util.List;
/**
* 管理所有的Activity
*/
public class ActivityManager {
//保存所有创建的Activity
private List<Activity> allActivities = new ArrayList<>();
/**
* 添加Activity到管理器
*
* @param activity activity
*/
public void addActivity(Activity activity) {
if (activity != null) {
allActivities.add(activity);
}
}
/**
* 从管理器移除Activity
*
* @param activity activity
*/
public void removeActivity(Activity activity) {
if (activity != null) {
allActivities.remove(activity);
}
}
/**
* 关闭所有Activity
*/
public void finishAll() {
for (Activity activity : allActivities) {
activity.finish();
}
}
public Activity getTaskTop() {
return allActivities.get(allActivities.size() - 1);
}
}
④ 创建BaseApplication
在模块的com.llw.mvplibrary包下新建一个BaseApplication继承Application,作为全局管理
代码如下:
package com.llw.mvplibrary;
import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
import com.llw.mvplibrary.utils.ActivityManager;
/**
* 工程管理
*/
public class BaseApplication extends Application {
private static ActivityManager activityManager;
private static BaseApplication application;
private static Context context;
@Override
public void onCreate() {
super.onCreate();
//声明Activity管理
activityManager=new ActivityManager();
context = getApplicationContext();
application=this;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
public static ActivityManager getActivityManager() {
return activityManager;
}
//内容提供器
public static Context getContext(){
return context;
}
public static BaseApplication getApplication() {
return application;
}
}
⑤ 创建KnifeKit
接下来创建一个kit包,包下创建一个KnifeKit类
代码如下:
package com.llw.mvplibrary.kit;
import android.app.Activity;
import android.app.Dialog;
import android.view.View;
import butterknife.ButterKnife;
import butterknife.Unbinder;
/**
* 绑定视图控件ID
*/
public class KnifeKit {
//解绑
public static Unbinder bind(Object target) {
if (target instanceof Activity) {
return ButterKnife.bind((Activity) target);
} else if (target instanceof Dialog) {
return ButterKnife.bind((Dialog) target);
} else if (target instanceof View) {
return ButterKnife.bind((View) target);
}
return Unbinder.EMPTY;
}
//绑定输入目标资源
public static Unbinder bind(Object target, Object source) {
if (source instanceof Activity) {
return ButterKnife.bind(target, (Activity) source);
} else if (source instanceof Dialog) {
return ButterKnife.bind(target, (Dialog) source);
} else if (source instanceof View) {
return ButterKnife.bind(target, (View) source);
}
return Unbinder.EMPTY;
}
//解绑
public static void unbind(Unbinder unbinder) {
if (unbinder != Unbinder.EMPTY) {
unbinder.unbind();
}
}
}
⑥ 创建base包(以及包下的类和接口)
接下来新建一个base包,下面创建一个UiCallBack接口
代码如下:
package com.llw.mvplibrary.base;
import android.os.Bundle;
/**
* UI回调接口
*/
public interface UiCallBack {
//初始化savedInstanceState
void initBeforeView(Bundle savedInstanceState);
//初始化
void initData(Bundle savedInstanceState);
//布局
int getLayoutId();
}
在base包,新创建一个BaseView接口
代码如下:
package com.llw.mvplibrary.base;
/**
* 只是一个接口BaseView ,里面可以自由定制
*/
public interface BaseView {
}
base包下面创建一个BasePresenter类
代码如下:
package com.llw.mvplibrary.base;
import com.llw.mvplibrary.base.BaseView;
import java.lang.ref.WeakReference;
/**
* Presenter基类 操作视图View
* @param <V>
*/
public class BasePresenter<V extends BaseView> {
private WeakReference<V> mWeakReference;
/**
* 关联view
* @param v
*/
public void attach(V v){
mWeakReference=new WeakReference<V>(v);
}
/**
* 分离view
* @param v
*/
public void detach(V v){
if (mWeakReference!=null){
mWeakReference.clear();
mWeakReference=null;
}
}
/**
* 获取view
* @return
*/
public V getView(){
if (mWeakReference!=null){
return mWeakReference.get();
}
return null;
}
}
接下来在base包下面创建一个网络请求返回解析基类 BaseResponse
代码如下:
package com.llw.mvplibrary.base;
/**
* @ClassDest: 网络请求返回解析基类
*/
public class BaseResponse {
/**
* code : 200
* msg : incorrect password
* data : null
*/
private int code;
private String msg;
private Object data;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
接下来在base包下面创建BaseActivity ,不需要MVP的Activity普通的Activity直接继承即可使用,这用主要是用于管理Acitivity
代码如下,这个里面还有进一步优化的空间,后面会提到的。
package com.llw.mvplibrary.base;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.llw.mvplibrary.BaseApplication;
import com.llw.mvplibrary.kit.KnifeKit;
import butterknife.Unbinder;
/**
* 用于不需要请求网络接口的Activity
*/
public abstract class BaseActivity extends AppCompatActivity implements UiCallBack {
protected Activity context;
private Unbinder unbinder;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initBeforeView(savedInstanceState);
this.context = this;
//添加继承这个BaseActivity的Activity
BaseApplication.getActivityManager().addActivity(this);
if (getLayoutId() > 0) {
setContentView(getLayoutId());
unbinder = KnifeKit.bind(this);
}
initData(savedInstanceState);
}
@Override
public void initBeforeView(Bundle savedInstanceState) {
}
@Override
protected void onStart() {
super.onStart();
}
}
既然有了BaseActivity,当然也要有BaseFragment,
在base包下创建BaseFragment意思与BaseActivity接近
代码如下:
package com.llw.mvplibrary.base;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.llw.mvplibrary.kit.KnifeKit;
import butterknife.Unbinder;
/**
* 用于不需要请求网络接口的BaseFragment
*/
public abstract class BaseFragment extends Fragment implements UiCallBack {
protected View rootView;
protected LayoutInflater layoutInflater;
protected Activity context;
private Unbinder unbinder;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initBeforeView(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
layoutInflater = inflater;
if (rootView == null) {
rootView = inflater.inflate(getLayoutId(), null);
unbinder = KnifeKit.bind(this, rootView);
} else {
ViewGroup viewGroup = (ViewGroup) rootView.getParent();
if (viewGroup != null) {
viewGroup.removeView(rootView);
}
}
return rootView;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
initData(savedInstanceState);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context instanceof Activity) {
this.context = (Activity) context;
}
}
@Override
public void onDetach() {
super.onDetach();
context = null;
}
@Override
public void initBeforeView(Bundle savedInstanceState) {
}
}
⑦ 创建mvp包(以及包下的Activity和Fragment)
base需要的东西已经写完了。接下来创建一个mvp包,包下创建MvpActivity
代码如下:
package com.llw.mvplibrary.mvp;
import android.os.Bundle;
import com.llw.mvplibrary.base.BaseActivity;
import com.llw.mvplibrary.base.BasePresenter;
import com.llw.mvplibrary.base.BaseView;
/**
* 适用于需要访问网络接口的Activity
*/
public abstract class MvpActivity<P extends BasePresenter> extends BaseActivity {
protected P mPresent;
@Override
public void initBeforeView(Bundle savedInstanceState) {
mPresent=createPresent();
mPresent.attach((BaseView) this);
}
protected abstract P createPresent();
@Override
public void onDestroy() {
super.onDestroy();
mPresent.detach((BaseView) this);
}
}
同样在mvp包下创建MvpFragment
代码如下:
package com.llw.mvplibrary.mvp;
import android.os.Bundle;
import com.llw.mvplibrary.base.BaseFragment;
import com.llw.mvplibrary.base.BasePresenter;
import com.llw.mvplibrary.base.BaseView;
/**
* 适用于需要访问网络接口的Fragment
*/
public abstract class MvpFragment<P extends BasePresenter> extends BaseFragment {
protected P mPresent;
@Override
public void initBeforeView(Bundle savedInstanceState) {
mPresent=createPresent();
mPresent.attach((BaseView) this);
}
@Override
public void onDetach() {
super.onDetach();
if (mPresent!=null){
mPresent.detach((BaseView) this);
}
}
protected abstract P createPresent();
}
⑧ 创建net包(封装OKHttp,重写CallBack)
mvp包下的内容写完了,接下来配置网络访问
先创建一个net包 ,在这个包下新建一个ServiceGenerator类
代码如下:
package com.llw.mvplibrary.net;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
/**
* 服务构建器 API服务设置在里面
*/
public class ServiceGenerator {
//https://free-api.heweather.net/s6/weather/now?key=3086e91d66c04ce588a7f538f917c7f4&location=深圳
//将上方的API接口地址进行拆分得到不变的一部分,实际开发中可以将这一部分作为服务器的ip访问地址
public static String BASE_URL = "https://free-api.heweather.net";//地址
//创建服务 参数就是API服务
public static <T> T createService(Class<T> serviceClass) {
//创建OkHttpClient构建器对象
OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
//设置请求超时的时间,这里是10秒
okHttpClientBuilder.connectTimeout(10000, TimeUnit.MILLISECONDS);
//消息拦截器 因为有时候接口不同在排错的时候 需要先从接口的响应中做分析。利用了消息拦截器可以清楚的看到接口返回的所有内容
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
//setlevel用来设置日志打印的级别,共包括了四个级别:NONE,BASIC,HEADER,BODY
//BASEIC:请求/响应行
//HEADER:请求/响应行 + 头
//BODY:请求/响应航 + 头 + 体
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
//为OkHttp添加消息拦截器
okHttpClientBuilder.addInterceptor(httpLoggingInterceptor);
//在Retrofit中设置httpclient
Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL)//设置地址 就是上面的固定地址,如果你是本地访问的话,可以拼接上端口号 例如 +":8080"
.addConverterFactory(GsonConverterFactory.create())//用Gson把服务端返回的json数据解析成实体
.client(okHttpClientBuilder.build())//放入OKHttp,之前说过retrofit是对OkHttp的进一步封装
.build();
return retrofit.create(serviceClass);//返回这个创建好的API服务
}
}
接下来重写Callback,在,net包下新建NetCallBack类
代码如下:
package com.llw.mvplibrary.net;
import android.util.Log;
import com.google.gson.Gson;
import com.llw.mvplibrary.base.BaseResponse;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* 网络请求回调
* @param <T>
*/
public abstract class NetCallBack<T> implements Callback<T> {//这里实现了retrofit2.Callback
//访问成功回调
@Override
public void onResponse(Call<T> call, Response<T> response) {//数据返回
if (response != null && response.body() != null && response.isSuccessful()) {
BaseResponse baseResponse = new Gson().fromJson(new Gson().toJson(response.body()), BaseResponse.class);
if (baseResponse.getCode() == 404) {//404
Log.e("Warn",baseResponse.getData().toString());
}else if(baseResponse.getCode() == 500) {//500
Log.e("Warn",baseResponse.getData().toString());
} else {//无异常则返回数据
onSuccess(call, response);
Log.e("Warn","其他情况");
}
} else {
onFailed();
}
}
//访问失败回调
@Override
public void onFailure(Call<T> call, Throwable t) {
onFailed();
}
//数据返回
public abstract void onSuccess(Call<T> call, Response<T> response);
//失败异常
public abstract void onFailed();
}
5. app使用MVP
至此,MVP框架就搭建完成了,接下来回到app项目中在com.llw.goodweather包下创建一个api包,在这个包下新建一个ApiService接口
① 创建API管理服务接口ApiService
代码如下:
package com.llw.goodweather.api;
import com.llw.goodweather.bean.TodayResponse;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
/**
* API服务接口
*/
public interface ApiService {
/**
* 当天天气查询
* https://free-api.heweather.net/s6/weather/now?key=3086e91d66c04ce588a7f538f917c7f4&location=深圳
* 将地址进一步拆分,将可变的一部分放在注解@GET的地址里面,其中
* /s6/weather/now?key=3086e91d66c04ce588a7f538f917c7f4 这一部分在这个接口中又是不变的,变的是location的值
* 所以将location的参数放入@Query里面,因为是使用的GET请求,所以里面的内容会拼接到地址后面,并且自动会加上 & 符号
* Call是retrofit2框架里面的,这个框架是对OKHttp的进一步封装,会让你的使用更加简洁明了,里面放入之前通过接口返回
* 的JSON字符串生成返回数据实体Bean,Retrofit支持Gson解析实体类,所以,后面的返回值就不用做解析了。
* getTodayWeather是这个接口的方法名。这样说应该很清楚了吧
* @param location 区/县
* @return
*/
@GET("/s6/weather/now?key=3086e91d66c04ce588a7f538f917c7f4")
Call<TodayResponse> getTodayWeather(@Query("location") String location);
}
这里你要注意一点key的值用你自己的应用的KEY 。
② 订阅接口服务,处理API请求返回数据
接下来新建contract包,创建一个订阅器WeatherContract类
代码如下:
package com.llw.goodweather.contract;
import android.content.Context;
import com.llw.goodweather.api.ApiService;
import com.llw.goodweather.bean.TodayResponse;
import com.llw.mvplibrary.base.BasePresenter;
import com.llw.mvplibrary.base.BaseView;
import com.llw.mvplibrary.net.NetCallBack;
import com.llw.mvplibrary.net.ServiceGenerator;
import retrofit2.Call;
import retrofit2.Response;
/**
* 天气订阅器
*/
public class WeatherContract {
public static class WeatherPresenter extends BasePresenter<IWeatherView> {
/**
* 当日天气
* @param context
* @param location 区/县
*/
public void todayWeather(final Context context, String location) {
//得到构建之后的网络请求服务,这里的地址已经拼接完成,只差一个location了
ApiService service = ServiceGenerator.createService(ApiService.class);
//设置请求回调 NetCallBack是重写请求回调
service.getTodayWeather(location).enqueue(new NetCallBack<TodayResponse>() {
//成功回调
@Override
public void onSuccess(Call<TodayResponse> call, Response<TodayResponse> response) {
if (getView() != null) {//当视图不会空时返回请求数据
getView().getTodayWeatherResult(response);
}
}
//失败回调
@Override
public void onFailed() {
if (getView() != null) {//当视图不会空时获取错误信息
getView().getDataFailed();
}
}
});
}
}
public interface IWeatherView extends BaseView {
//将数据放入实体
void getTodayWeatherResult(Response<TodayResponse> response);
//错误返回
void getDataFailed();
}
}
③ 继承mvplibrary中的BaseApplication
接下来,在项目的com.llw.goodweather包下,新建一个WeatherApplication类继承模块中BaseApplication
代码如下:
package com.llw.goodweather;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.webkit.WebView;
import com.llw.mvplibrary.BaseApplication;
import com.llw.mvplibrary.utils.ActivityManager;
import com.scwang.smartrefresh.layout.SmartRefreshLayout;
import com.scwang.smartrefresh.layout.api.DefaultRefreshFooterCreator;
import com.scwang.smartrefresh.layout.api.DefaultRefreshHeaderCreator;
import com.scwang.smartrefresh.layout.api.RefreshFooter;
import com.scwang.smartrefresh.layout.api.RefreshHeader;
import com.scwang.smartrefresh.layout.api.RefreshLayout;
import com.scwang.smartrefresh.layout.footer.ClassicsFooter;
import com.scwang.smartrefresh.layout.header.ClassicsHeader;
public class WeatherApplication extends BaseApplication {
/**
* 应用实例
*/
public static WeatherApplication weatherApplication;
private static Context context;
private static ActivityManager activityManager;
private static Activity sActivity;
public static Context getMyContext() {
return weatherApplication == null ? null : weatherApplication.getApplicationContext();
}
private Handler myHandler;
public Handler getMyHandler() {
return myHandler;
}
public void setMyHandler(Handler handler) {
myHandler = handler;
}
@Override
public void onCreate() {
super.onCreate();
activityManager = new ActivityManager();
context = getApplicationContext();
weatherApplication = this;
this.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
sActivity = activity;
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
}
public static ActivityManager getActivityManager() {
return activityManager;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
//static 代码段可以防止内存泄露
static {
//设置全局的Header构建器
SmartRefreshLayout.setDefaultRefreshHeaderCreator(new DefaultRefreshHeaderCreator() {
@Override
public RefreshHeader createRefreshHeader(Context context, RefreshLayout layout) {
layout.setPrimaryColorsId(android.R.color.darker_gray, android.R.color.black);//全局设置主题颜色
return new ClassicsHeader(context);//.setTimeFormat(new DynamicTimeFormat("更新于 %s"));//指定为经典Header,默认是 贝塞尔雷达Header
}
});
//设置全局的Footer构建器
SmartRefreshLayout.setDefaultRefreshFooterCreator(new DefaultRefreshFooterCreator() {
@Override
public RefreshFooter createRefreshFooter(Context context, RefreshLayout layout) {
//指定为经典Footer,默认是 BallPulseFooter
return new ClassicsFooter(context).setDrawableSize(20);
}
});
}
}
④ 配置AndroidManifest.xml文件
接下来在AndroidManifest.xml文件中配置WeatherApplication
由于Android9.0以后网络访问默认是https了,导致访问http类型的API接口访问不了,所以要配置项目允许访问http,所以在res文件下面新建一个xml的文件夹,在这个文件夹下新建名为的network_security_config.xml的网络配置文件,里面的配置代码如下:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
接下来在AndroidManifest.xml文件中配置
现在你可以运行一下,看你的项目有没有问题,早出现问题早解决。
现在框架已经搭好了,不过页面布局还没有写好的,所以要写一下页面了。
⑤ 编辑布局文件
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:gravity="center"
android:fitsSystemWindows="true"
android:background="@drawable/pic_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!--相对布局-->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--透明度为0.3的黑色背景-->
<LinearLayout
android:background="#000"
android:alpha="0.3"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!--主要的布局文件-->
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--标题 沉浸式-->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:contentInsetLeft="16dp"
app:popupTheme="@style/AppTheme.PopupOverlay">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="16sp"
android:textColor="#FFF"
android:text="城市天气" />
</androidx.appcompat.widget.Toolbar>
<!--天气和所在城市 -->
<LinearLayout
android:gravity="center_horizontal"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--天气状况-->
<TextView
android:paddingLeft="16dp"
android:paddingTop="12dp"
android:id="@+id/tv_info"
android:textColor="#FFF"
android:textSize="18sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<!--温度-->
<LinearLayout
android:gravity="top|center_horizontal"
android:layout_marginTop="20dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_temperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textColor="#FFF"
android:textSize="60sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="℃"
android:textColor="#FFF"
android:textSize="24sp" />
</LinearLayout>
<!--最高温和最低温-->
<TextView
android:layout_marginTop="12dp"
android:id="@+id/tv_low_height"
android:textColor="#FFF"
android:textSize="@dimen/sp_14"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<!--城市-->
<TextView
android:layout_marginTop="20dp"
android:id="@+id/tv_city"
android:textColor="#FFF"
android:text="城市"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<!--上一次更新时间-->
<TextView
android:layout_marginTop="8dp"
android:id="@+id/tv_old_time"
android:textColor="#FFF"
android:text="上次更新时间:"
android:textSize="@dimen/sp_12"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
背景图
修改res文件下styles.xml文件
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>
这样你的布局文件应该就没有报错的红线了。
然后看到MainActivity.java中的这个TextView报错,因为布局文件中已经去掉了这个TextView。
删除即可
然后再绑定布局中控件
@BindView(R.id.tv_info)
TextView tvInfo;//天气状况
@BindView(R.id.tv_temperature)
TextView tvTemperature;//温度
@BindView(R.id.tv_low_height)
TextView tvLowHeight;//最高温和最低温
@BindView(R.id.tv_city)
TextView tvCity;//城市
@BindView(R.id.tv_old_time)
TextView tvOldTime;//最近更新时间
⑥ 天气查询(使用MVPActivity实现数据请求与数据渲染显示)
接下来进行使用MVP框架数据请求,删除getTodayWeather()方法。修改后的MainActivity代码如下所示
package com.llw.goodweather;
import android.Manifest;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baidu.location.BDAbstractLocationListener;
import com.baidu.location.BDLocation;
import com.baidu.location.LocationClient;
import com.baidu.location.LocationClientOption;
import com.llw.goodweather.bean.TodayResponse;
import com.llw.goodweather.contract.WeatherContract;
import com.llw.goodweather.utils.ToastUtils;
import com.llw.mvplibrary.mvp.MvpActivity;
import com.tbruyelle.rxpermissions2.RxPermissions;
import java.io.IOException;
import butterknife.BindView;
import butterknife.ButterKnife;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import retrofit2.Response;
public class MainActivity extends MvpActivity<WeatherContract.WeatherPresenter> implements WeatherContract.IWeatherView {
@BindView(R.id.tv_info)
TextView tvInfo;//天气状况
@BindView(R.id.tv_temperature)
TextView tvTemperature;//温度
@BindView(R.id.tv_low_height)
TextView tvLowHeight;//最高温和最低温
@BindView(R.id.tv_city)
TextView tvCity;//城市
@BindView(R.id.tv_old_time)
TextView tvOldTime;//最近更新时间
private RxPermissions rxPermissions;//权限请求框架
//定位器
public LocationClient mLocationClient = null;
private MyLocationListener myListener = new MyLocationListener();
//数据初始化 主线程,onCreate方法可以删除了,把里面的代码移动这个initData下面
@Override
public void initData(Bundle savedInstanceState) {
//因为这个框架里面已经放入了绑定,所以这行代码可以注释掉了。
//ButterKnife.bind(this);
rxPermissions = new RxPermissions(this);//实例化这个权限请求框架,否则会报错
permissionVersion();//权限判断
}
//绑定布局文件
@Override
public int getLayoutId() {
return R.layout.activity_main;
}
//绑定Presenter ,这里不绑定会报错
@Override
protected WeatherContract.WeatherPresenter createPresent() {
return new WeatherContract.WeatherPresenter();
}
//权限判断
private void permissionVersion() {
if (Build.VERSION.SDK_INT >= 23) {//6.0或6.0以上
//动态权限申请
permissionsRequest();
} else {//6.0以下
//发现只要权限在AndroidManifest.xml中注册过,均会认为该权限granted 提示一下即可
ToastUtils.showShortToast(this, "你的版本在Android6.0以下,不需要动态申请权限。");
}
}
//动态权限申请
private void permissionsRequest() {//使用这个框架需要制定JDK版本,建议用1.8
rxPermissions.request(Manifest.permission.ACCESS_FINE_LOCATION)
.subscribe(granted -> {
if (granted) {//申请成功
//得到权限之后开始定位
startLocation();
} else {//申请失败
ToastUtils.showShortToast(this, "权限未开启");
}
});
}
//定位
private void startLocation() {
//声明LocationClient类
mLocationClient = new LocationClient(this);
//注册监听函数
mLocationClient.registerLocationListener(myListener);
LocationClientOption option = new LocationClientOption();
//如果开发者需要获得当前点的地址信息,此处必须为true
option.setIsNeedAddress(true);
//可选,设置是否需要最新版本的地址信息。默认不需要,即参数为false
option.setNeedNewVersionRgc(true);
//mLocationClient为第二步初始化过的LocationClient对象
//需将配置好的LocationClientOption对象,通过setLocOption方法传递给LocationClient对象使用
mLocationClient.setLocOption(option);
//启动定位
mLocationClient.start();
}
/**
* 定位结果返回
*/
private class MyLocationListener extends BDAbstractLocationListener {
@Override
public void onReceiveLocation(BDLocation location) {
//获取区/县
String district = location.getDistrict();
//获取今天的天气数据
mPresent.todayWeather(context,district);
}
}
//查询当天天气,请求成功后的数据返回
@Override
public void getTodayWeatherResult(Response<TodayResponse> response) {
//数据返回后关闭定位
mLocationClient.stop();
if (response.body().getHeWeather6().get(0).getBasic() != null) {//得到数据不为空则进行数据显示
//数据渲染显示出来
tvTemperature.setText(response.body().getHeWeather6().get(0).getNow().getTmp());//温度
tvCity.setText(response.body().getHeWeather6().get(0).getBasic().getLocation());//城市
tvInfo.setText(response.body().getHeWeather6().get(0).getNow().getCond_txt());//天气状况
tvOldTime.setText("上次更新时间:" + response.body().getHeWeather6().get(0).getUpdate().getLoc());
} else {
ToastUtils.showShortToast(context, response.body().getHeWeather6().get(0).getStatus());
}
}
//数据请求失败返回
@Override
public void getDataFailed() {
ToastUtils.showShortToast(context,"网络异常");//这里的context是框架中封装好的,等同于this
}
}
写完之后就可以直接运行了,运行效果图如下:
可以看到,已经得到天气数据了,只不过美中不足,上面的状态栏是原生的颜色,原谅绿,这个颜色不吉利啊。我们换一下。这个时候就可以用到透明状态栏,这种东西了,在utils包下新建一个StatusBarUtil工具类
工具类代码如下:
package com.llw.goodweather.utils;
import android.annotation.TargetApi;
import android.app.Activity;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import com.readystatesoftware.systembartint.SystemBarTintManager;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* 状态栏工具类
*/
public class StatusBarUtil {
/**
* 修改状态栏为全透明
*
* @param activity
*/
@TargetApi(19)
public static void transparencyBar(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = activity.getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Window window = activity.getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}
/**
* 修改状态栏颜色,支持4.4以上版本
*
* @param activity
* @param colorId
*/
public static void setStatusBarColor(Activity activity, int colorId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = activity.getWindow();
// window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(activity.getResources().getColor(colorId));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//使用SystemBarTint库使4.4版本状态栏变色,需要先将状态栏设置为透明
transparencyBar(activity);
SystemBarTintManager tintManager = new SystemBarTintManager(activity);
tintManager.setStatusBarTintEnabled(true);
tintManager.setStatusBarTintResource(colorId);
}
}
/**
* 状态栏亮色模式,设置状态栏黑色文字、图标,
* 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
*
* @param activity
* @return 1:MIUUI 2:Flyme 3:android6.0
*/
public static int StatusBarLightMode(Activity activity) {
int result = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (MIUISetStatusBarLightMode(activity, true)) {
result = 1;
} else if (FlymeSetStatusBarLightMode(activity.getWindow(), true)) {
result = 2;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
result = 3;
}
}
return result;
}
/**
* 已知系统类型时,设置状态栏黑色文字、图标。
* 适配4.4以上版本MIUIV、Flyme和6.0以上版本其他Android
*
* @param activity
* @param type 1:MIUUI 2:Flyme 3:android6.0
*/
public static void StatusBarLightMode(Activity activity, int type) {
if (type == 1) {
MIUISetStatusBarLightMode(activity, true);
} else if (type == 2) {
FlymeSetStatusBarLightMode(activity.getWindow(), true);
} else if (type == 3) {
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
}
/**
* 状态栏暗色模式,清除MIUI、flyme或6.0以上版本状态栏黑色文字、图标
*/
public static void StatusBarDarkMode(Activity activity, int type) {
if (type == 1) {
MIUISetStatusBarLightMode(activity, false);
} else if (type == 2) {
FlymeSetStatusBarLightMode(activity.getWindow(), false);
} else if (type == 3) {
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
}
}
/**
* 设置状态栏图标为深色和魅族特定的文字风格
* 可以用来判断是否为Flyme用户
*
* @param window 需要设置的窗口
* @param dark 是否把状态栏文字及图标颜色设置为深色
* @return boolean 成功执行返回true
*/
public static boolean FlymeSetStatusBarLightMode(Window window, boolean dark) {
boolean result = false;
if (window != null) {
try {
WindowManager.LayoutParams lp = window.getAttributes();
Field darkFlag = WindowManager.LayoutParams.class
.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
Field meizuFlags = WindowManager.LayoutParams.class
.getDeclaredField("meizuFlags");
darkFlag.setAccessible(true);
meizuFlags.setAccessible(true);
int bit = darkFlag.getInt(null);
int value = meizuFlags.getInt(lp);
if (dark) {
value |= bit;
} else {
value &= ~bit;
}
meizuFlags.setInt(lp, value);
window.setAttributes(lp);
result = true;
} catch (Exception e) {
}
}
return result;
}
/**
* 需要MIUIV6以上
*
* @param activity
* @param dark 是否把状态栏文字及图标颜色设置为深色
* @return boolean 成功执行返回true
*/
public static boolean MIUISetStatusBarLightMode(Activity activity, boolean dark) {
boolean result = false;
Window window = activity.getWindow();
if (window != null) {
Class clazz = window.getClass();
try {
int darkModeFlag = 0;
Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
darkModeFlag = field.getInt(layoutParams);
Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
if (dark) {
extraFlagField.invoke(window, darkModeFlag, darkModeFlag);//状态栏透明且黑色字体
} else {
extraFlagField.invoke(window, 0, darkModeFlag);//清除黑色字体
}
result = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//开发版 7.7.13 及以后版本采用了系统API,旧方法无效但不会报错,所以两个方式都要加上
if (dark) {
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
} else {
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
}
}
} catch (Exception e) {
}
}
return result;
}
}
接下来,在MainActivity.java中调用即可
StatusBarUtil.transparencyBar(context);//透明状态栏
然后再运行一下
不管怎么说,都比原谅绿好看。
现在查询当天的天气是可以了,但是都说是天气预报了,当然也要有啊,否则不就是骗人了吗?OK
源码地址:GoodWeather
欢迎 Star 和 Fork