上一篇:Android 天气APP(三)访问天气API与数据请求

MVP/MVVM框架搭建与使用

  • 新版-------------------
    • 一、创建依赖模块
    • 二、模块初始化
    • 三、搭建网络框架
    • 四、基础封装
    • 五、使用
    • 六、文章源码
  • 旧版-------------------
    • 4. MVP框架搭建
      • ① 创建模块
      • ② 配置模块
      • ③ 创建Activity管理
      • ④ 创建BaseApplication
      • ⑤ 创建KnifeKit
      • ⑥ 创建base包(以及包下的类和接口)
      • ⑦ 创建mvp包(以及包下的Activity和Fragment)
      • ⑧ 创建net包(封装OKHttp,重写CallBack)
    • 5. app使用MVP
      • ① 创建API管理服务接口ApiService
      • ② 订阅接口服务,处理API请求返回数据
      • ③ 继承mvplibrary中的BaseApplication
      • ④ 配置AndroidManifest.xml文件
      • ⑤ 编辑布局文件
      • ⑥ 天气查询(使用MVPActivity实现数据请求与数据渲染显示)

新版-------------------

  在上一篇文章中通过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 {//保存所有创建的Activityprivate 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;@Overridepublic 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{}闭包下添加如下所示代码:

 //OKHttpimplementation 'com.squareup.okhttp3:okhttp:3.14.9'//retrofit2api 'com.squareup.retrofit2:retrofit:2.9.0'//这里用api 是为了让其他模块也可以使用gsonimplementation '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'//rxjavaapi '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")@Exposepublic Integer responseCode;/*** 返回的错误信息*/@SerializedName("res_error")@Exposepublic String responseError;
}

  下面在network包下新建一个observer包,里面创建一个抽象类BaseObserver,实现rxjava的Observer接口,代码如下:

public abstract class BaseObserver<T> implements Observer<T> {//开始@Overridepublic void onSubscribe(Disposable d) {}//继续@Overridepublic void onNext(T t) {onSuccess(t);}//异常@Overridepublic void onError(Throwable e) {onFailure(e);}//完成@Overridepublic 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;}/*** 拦截*/@Overridepublic 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();/*** 拦截*/@Overridepublic 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,最后也会走到这个函数来统一处理;*/@Overridepublic 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;//retrofitHashMapprivate 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));//设置网络请求超时时长,这里设置为10sbuilder.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转化工厂自动转化成与其结构相符的实体Beanbuilder.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>() {@Overridepublic 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>() {@Overridepublic 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;@Overrideprotected 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;@Overrideprotected 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> {@Overridepublic 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;}/*** 版本名*/@Overridepublic String getAppVersionName() {return BuildConfig.VERSION_NAME;}/*** 版本号*/@Overridepublic String getAppVersionCode() {return String.valueOf(BuildConfig.VERSION_CODE);}/*** 是否为debug*/@Overridepublic boolean isDebug() {return BuildConfig.DEBUG;}/*** 应用全局上下文*/@Overridepublic 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<>() {@Overridepublic 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());}}@Overridepublic 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;/*** 注册意图*/@Overridepublic 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();}});}/*** 初始化*/@Overrideprotected void onCreate() {initLocation();requestPermission();viewModel = new ViewModelProvider(this).get(MainViewModel.class);}/*** 数据观察*/@Overrideprotected 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();//如果开发者需要获得当前点的地址信息,此处必须为trueoption.setIsNeedAddress(true);//可选,设置是否需要最新版本的地址信息。默认不需要,即参数为falseoption.setNeedNewVersionRgc(true);//需将配置好的LocationClientOption对象,通过setLocOption方法传递给LocationClient对象使用mLocationClient.setLocOption(option);}}/*** 开始定位*/private void startLocation() {if (mLocationClient != null) {mLocationClient.start();}}/*** 接收定位信息** @param bdLocation 定位数据*/@Overridepublic void onReceiveLocation(BDLocation bdLocation) {double latitude = bdLocation.getLatitude();    //获取纬度信息double longitude = bdLocation.getLongitude();    //获取经度信息float radius = bdLocation.getRadius();    //获取定位精度,默认值为0.0fString 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模块的目录。

检查一下是否一致。

六、文章源码

欢迎 StarFork

第四篇文章源码地址:GoodWeather-New-4

旧版-------------------

4. MVP框架搭建

现在这样固然符合网络请求的标准,结果也得到了,但是这只是一个接口而已,我们用了这么多代码,那假如这个页面上还有好几个接口要请求访问,岂不是多出了很多的重复代码,这一点并不符合现在Android的现状,所以需要封装OKHttp,通过架构或者框架来完成这一步,前期虽然麻烦一些,但是你一旦用习惯了,就停不下来了,接下来我尽量用人话来讲述这个搭建过程。
为了让你有一个清晰的思路,这里创建一个模块,里面搭建MVP框架。

① 创建模块

鼠标右键你的项目名,选择Module


点击Finish

现在模块就创建完成了。

② 配置模块

接下来修改模块的build.gradle
代码如下:

apply plugin: 'com.android.library'android {compileSdkVersion 28buildToolsVersion "28.0.3"defaultConfig {minSdkVersion 21targetSdkVersion 28versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"consumerProguardFiles 'consumer-rules.pro'}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}compileOptions {sourceCompatibility = 1.8targetCompatibility = 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'//支持okhttpapi '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'//阿里巴巴 FastJsonapi '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 {//保存所有创建的Activityprivate 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;@Overridepublic void onCreate() {super.onCreate();//声明Activity管理activityManager=new ActivityManager();context = getApplicationContext();application=this;}@Overridepublic 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 {//初始化savedInstanceStatevoid 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;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);initBeforeView(savedInstanceState);this.context = this;//添加继承这个BaseActivity的ActivityBaseApplication.getActivityManager().addActivity(this);if (getLayoutId() > 0) {setContentView(getLayoutId());unbinder = KnifeKit.bind(this);}initData(savedInstanceState);}@Overridepublic void initBeforeView(Bundle savedInstanceState) {}@Overrideprotected 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;@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);initBeforeView(savedInstanceState);}@Nullable@Overridepublic 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;}@Overridepublic void onActivityCreated(@Nullable Bundle savedInstanceState) {super.onActivityCreated(savedInstanceState);initData(savedInstanceState);}@Overridepublic void onAttach(Context context) {super.onAttach(context);if (context instanceof Activity) {this.context = (Activity) context;}}@Overridepublic void onDetach() {super.onDetach();context = null;}@Overridepublic 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;@Overridepublic void initBeforeView(Bundle savedInstanceState) {mPresent=createPresent();mPresent.attach((BaseView) this);}protected abstract P createPresent();@Overridepublic 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;@Overridepublic void initBeforeView(Bundle savedInstanceState) {mPresent=createPresent();mPresent.attach((BaseView) this);}@Overridepublic 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中设置httpclientRetrofit 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//访问成功回调@Overridepublic 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) {//404Log.e("Warn",baseResponse.getData().toString());}else if(baseResponse.getCode() == 500) {//500Log.e("Warn",baseResponse.getData().toString());} else {//无异常则返回数据onSuccess(call, response);Log.e("Warn","其他情况");}} else {onFailed();}}//访问失败回调@Overridepublic 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>() {//成功回调@Overridepublic void onSuccess(Call<TodayResponse> call, Response<TodayResponse> response) {if (getView() != null) {//当视图不会空时返回请求数据getView().getTodayWeatherResult(response);}}//失败回调@Overridepublic 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;}@Overridepublic void onCreate() {super.onCreate();activityManager = new ActivityManager();context = getApplicationContext();weatherApplication = this;this.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {@Overridepublic void onActivityCreated(Activity activity, Bundle savedInstanceState) {}@Overridepublic void onActivityStarted(Activity activity) {sActivity = activity;}@Overridepublic void onActivityResumed(Activity activity) {}@Overridepublic void onActivityPaused(Activity activity) {}@Overridepublic void onActivityStopped(Activity activity) {}@Overridepublic void onActivitySaveInstanceState(Activity activity, Bundle outState) {}@Overridepublic void onActivityDestroyed(Activity activity) {}});}public static ActivityManager getActivityManager() {return activityManager;}@Overridepublic void onConfigurationChanged(Configuration newConfig) {super.onConfigurationChanged(newConfig);}//static 代码段可以防止内存泄露static {//设置全局的Header构建器SmartRefreshLayout.setDefaultRefreshHeaderCreator(new DefaultRefreshHeaderCreator() {@Overridepublic 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() {@Overridepublic RefreshFooter createRefreshFooter(Context context, RefreshLayout layout) {//指定为经典Footer,默认是 BallPulseFooterreturn 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"><!--相对布局--><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"><!--透明度为0.3的黑色背景--><LinearLayoutandroid:background="#000"android:alpha="0.3"android:layout_width="match_parent"android:layout_height="match_parent"/><!--主要的布局文件--><LinearLayoutandroid:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><!--标题 沉浸式--><androidx.appcompat.widget.Toolbarandroid:id="@+id/toolbar"android:layout_width="match_parent"android:layout_height="?attr/actionBarSize"app:contentInsetLeft="16dp"app:popupTheme="@style/AppTheme.PopupOverlay"><TextViewandroid: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><!--天气和所在城市 --><LinearLayoutandroid:gravity="center_horizontal"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><!--天气状况--><TextViewandroid: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"/><!--温度--><LinearLayoutandroid:gravity="top|center_horizontal"android:layout_marginTop="20dp"android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="wrap_content"><TextViewandroid:id="@+id/tv_temperature"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="0"android:textColor="#FFF"android:textSize="60sp" /><TextViewandroid:layout_width="wrap_content"android:layout_height="match_parent"android:text="℃"android:textColor="#FFF"android:textSize="24sp" /></LinearLayout><!--最高温和最低温--><TextViewandroid: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"/><!--城市--><TextViewandroid: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"/><!--上一次更新时间--><TextViewandroid: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下面@Overridepublic void initData(Bundle savedInstanceState) {//因为这个框架里面已经放入了绑定,所以这行代码可以注释掉了。//ButterKnife.bind(this);rxPermissions = new RxPermissions(this);//实例化这个权限请求框架,否则会报错permissionVersion();//权限判断}//绑定布局文件@Overridepublic int getLayoutId() {return R.layout.activity_main;}//绑定Presenter ,这里不绑定会报错@Overrideprotected 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.8rxPermissions.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();//如果开发者需要获得当前点的地址信息,此处必须为trueoption.setIsNeedAddress(true);//可选,设置是否需要最新版本的地址信息。默认不需要,即参数为falseoption.setNeedNewVersionRgc(true);//mLocationClient为第二步初始化过的LocationClient对象//需将配置好的LocationClientOption对象,通过setLocOption方法传递给LocationClient对象使用mLocationClient.setLocOption(option);//启动定位mLocationClient.start();}/*** 定位结果返回*/private class MyLocationListener extends BDAbstractLocationListener {@Overridepublic void onReceiveLocation(BDLocation location) {//获取区/县String district = location.getDistrict();//获取今天的天气数据mPresent.todayWeather(context,district);}}//查询当天天气,请求成功后的数据返回@Overridepublic 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());}}//数据请求失败返回@Overridepublic 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
欢迎 StarFork

下一篇:Android 天气APP(五)天气预报、生活指数的数据请求与渲染

Android 天气APP(四)搭建MVP框架与使用相关推荐

  1. Android 天气APP(三十四)语音搜索

    上一篇:Android 天气APP(三十三)语音播报 语音搜索 前言 正文 一.权限配置 二.用户体验优化 三.配置语音识别听写 四.语音搜索 五.地图天气添加语音搜索功能 六.城市搜索添加语音搜索功 ...

  2. Android 天气APP(十四)修复UI显示异常、优化业务代码逻辑、增加详情天气显示

    上一篇:Android 天气APP(十三)仿微信弹窗(右上角加号点击弹窗效果).自定义背景图片.UI优化调整 添加管理城市 新版------------------- 一.添加管理城市页面 二.沉浸式 ...

  3. Android 天气APP(五)天气预报、生活指数的数据请求与渲染

    上一篇:Android 天气APP(四)搭建MVP框架与使用 天气预报.生活指数的数据请求与渲染 新版------------------- 一.增加天气接口地址 二.增加API接口 三.天气数据存储 ...

  4. Android 天气APP(三)访问天气API与数据请求

    上一篇:Android 天气APP(二)获取定位信息 访问天气API与数据请求 新版------------------- 一.和风天气 二.城市搜索接口 三.OKHttp使用 四.文章源码 旧版-- ...

  5. Android 天气APP(十五)增加城市搜索、历史搜索记录

    上一篇:Android 天气APP(十四)修复UI显示异常.优化业务代码逻辑.增加详情天气显示 添加城市 新版------------------- 一.推荐城市数据 二.推荐城市item布局和适配器 ...

  6. Android 天气APP(九)细节优化、必应每日一图

    上一篇:Android 天气APP(八)城市切换 之 自定义弹窗与使用 重新定位.必应每日一图 新版------------------- 一.封装定位 二.重新定位 三.必应每日一图 ① 添加必应接 ...

  7. Android 天气APP(一)开发准备

    好天气APP(天气预报.空气质量.生活建议.灾害预警.出行建议.城市切换.城市搜索.世界国家/地区的城市.常用城市.背景更换.应用自动更新) (运用百度定位.百度地图与和风天气API制作) 演示视频地 ...

  8. Android 天气APP(十三)仿微信弹窗(右上角加号点击弹窗效果)、自定义背景图片、UI优化调整

    上一篇:Android 天气APP(十二)空气质量.UI优化调整 天气预报详情,逐小时预报详情 新版------------------- 一.适配器点击监听 二.页面实现 三.天气预报详情弹窗 四. ...

  9. Android 天气APP(二十三)增加灾害预警、优化主页面UI

    上一篇:Android 天气APP(二十二)改动些许UI.增加更多空气质量数据和生活建议数据展示 文章目录 效果图 前言 一.灾害预警 1.数据实体 2.新增API和方法 3.数据渲染 4.灾害预报详 ...

最新文章

  1. 子窗口关闭,父窗口有选择刷新
  2. cadence中元件所在库
  3. android点击运行后无法显示设备,Android仿真器除了黑屏外什么都不显示,adb设备显示“设备离线”...
  4. 影子卫士和影子系统哪个好用_影子系统是什么?会损害电脑硬盘吗?
  5. WeTest功能优化第3期:业内首创,有声音的云真机
  6. 深度佳能MP4视频恢复软件 v8.1.0
  7. asp.net的几种页面间的传值方法
  8. 用C语言程序实现黎曼和求定积分
  9. Python 为什么要用线程池?
  10. iOS学习笔记-地图MapKit入门
  11. phpstorm如何同时打开两个文件夹_iPhone如何同时添加两个不同的面容ID?
  12. Leetcode每日一题:100.same-tree(相同的树)
  13. 传统的主从复制的概念和要点
  14. 用一个URL加一个JAVA壳做成一个安卓应用
  15. 【jQuery】jQuery操作input的聚焦与全选其内容
  16. ubuntu安装deep-wine下载qq,tim等文件
  17. 火狐浏览器 Mozilla Firefox v36.0.3 便携增强版
  18. 短连接生成系统如何设计?
  19. Bootloader的启动
  20. 拖拉机(寒假每日一题 4)

热门文章

  1. 3、强调、粗体、斜体
  2. 发那科机器人点位编辑_FANUC机器人零点快速校准流程——发那科机器人
  3. mxnet:结合R与GPU加速深度学习
  4. 使用U盘安装最新的UBUNTU等linux系统
  5. 【无标题】数学实验 慕课答案 第一节
  6. 一个14年前【取名软件】的简单逆向过程
  7. 使用fir或者蒲公英来做分发测试
  8. cdr 表格自动填充文字_微信群聊上线接龙表格功能,老师一族的福音来了?
  9. oracle解锁表语句
  10. 数字图像处理(第二章)笔记