mirror of
https://github.com/ahmedeltaher/MVVM-Kotlin-Android-Architecture.git
synced 2026-03-13 08:03:40 +08:00
Updating gradle to 2.3 .
Updating RXjava to version 2 . Adding unit test for home presenter .
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
@@ -7,3 +8,4 @@
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
/.idea/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
apply plugin: 'com.android.application'
|
||||
//Dagger
|
||||
apply plugin: 'com.neenbedankt.android-apt'
|
||||
|
||||
//ButterKnife
|
||||
apply plugin: 'com.jakewharton.butterknife'
|
||||
//lamda
|
||||
@@ -59,10 +58,10 @@ dependencies {
|
||||
compile "com.android.support:design:${supportLibraryVersion}"
|
||||
//Dagger
|
||||
compile "com.google.dagger:dagger:${daggerVersion}"
|
||||
apt "com.google.dagger:dagger-compiler:${daggerVersion}"
|
||||
annotationProcessor "com.google.dagger:dagger-compiler:${daggerVersion}"
|
||||
//Butter knife
|
||||
compile "com.jakewharton:butterknife:${butterKnifeVersion}"
|
||||
apt "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"
|
||||
annotationProcessor "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"
|
||||
//event bus
|
||||
compile 'com.squareup:otto:1.3.8'
|
||||
//Logging
|
||||
@@ -81,9 +80,9 @@ dependencies {
|
||||
compile "com.hannesdorfmann.parcelableplease:annotation:${parcelablepleaseVersion}"
|
||||
compile "com.hannesdorfmann.parcelableplease:processor:${parcelablepleaseVersion}"
|
||||
//Android RX
|
||||
compile 'io.reactivex:rxandroid:1.2.1'
|
||||
compile 'io.reactivex:rxjava:1.1.6'
|
||||
compile "com.squareup.retrofit2:adapter-rxjava:${retrofitVersion}"
|
||||
compile 'io.reactivex.rxjava2:rxjava:2.0.7'
|
||||
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
||||
compile 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
|
||||
// espresso , junit for testing
|
||||
testCompile 'junit:junit:4.12'
|
||||
compile('com.android.support.test:rules:0.5') {
|
||||
@@ -115,4 +114,7 @@ dependencies {
|
||||
exclude group: "javax.inject", module: "javax.inject"
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
//Mockito
|
||||
testCompile "org.mockito:mockito-core:${mockitoVersion}"
|
||||
androidTestCompile "org.mockito:mockito-core:${mockitoVersion}"
|
||||
}
|
||||
@@ -9,12 +9,13 @@ ext {
|
||||
targetSdkVersion = 24
|
||||
|
||||
//Libraries
|
||||
buildToolsVersion = '24.0.1'
|
||||
supportLibraryVersion = '24.2.1'
|
||||
buildToolsVersion = '25.0.2'
|
||||
supportLibraryVersion = '25.1.1'
|
||||
daggerVersion = '2.0.2'
|
||||
parcelablepleaseVersion = '1.0.2'
|
||||
espressoVersion = '2.2.2'
|
||||
retrofitVersion = '2.2.0'
|
||||
okhttpVersion = '3.3.0'
|
||||
butterKnifeVersion = '8.5.1'
|
||||
mockitoVersion = '2.7.1'
|
||||
}
|
||||
@@ -6,8 +6,9 @@ import com.task.data.remote.dto.NewsModel;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import rx.Observable;
|
||||
import rx.schedulers.Schedulers;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
|
||||
/**
|
||||
* Created by AhmedEltaher on 5/12/2016
|
||||
|
||||
@@ -10,13 +10,18 @@ import com.task.data.remote.service.NewsService;
|
||||
import com.task.utils.Constants;
|
||||
import com.task.utils.L;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.reactivestreams.Subscriber;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.ObservableEmitter;
|
||||
import io.reactivex.ObservableOnSubscribe;
|
||||
import io.reactivex.Observer;
|
||||
import retrofit2.Call;
|
||||
import rx.Observable;
|
||||
import rx.Subscriber;
|
||||
|
||||
import static com.task.data.remote.ServiceError.NETWORK_ERROR;
|
||||
import static com.task.utils.Constants.ERROR_UNDEFINED;
|
||||
@@ -37,19 +42,31 @@ public class ApiRepository {
|
||||
|
||||
public Observable getNews() {
|
||||
|
||||
Observable<NewsModel> newsObservable = Observable.create(new Observable.OnSubscribe<NewsModel>() {
|
||||
@Override
|
||||
public void call(Subscriber<? super NewsModel> subscriber) {
|
||||
if (!isConnected(App.getContext())) {
|
||||
Exception e = new NetworkErrorException();
|
||||
subscriber.onError(e);
|
||||
} else {
|
||||
NewsService newsService = serviceGenerator.createService(NewsService.class, Constants.BASE_URL);
|
||||
ServiceResponse serviceResponse = processCall(newsService.fetchNews(), false);
|
||||
NewsModel newsModel = (NewsModel) serviceResponse.getData();
|
||||
subscriber.onNext(newsModel);
|
||||
subscriber.onCompleted();
|
||||
}
|
||||
// Observable<NewsModel> newsObservable = Observable.create(new Observable.OnSubscribe<NewsModel>() {
|
||||
// @Override
|
||||
// public void call(Subscriber<? super NewsModel> subscriber) {
|
||||
// if (!isConnected(App.getContext())) {
|
||||
// Exception e = new NetworkErrorException();
|
||||
// subscriber.onError(e);
|
||||
// } else {
|
||||
// NewsService newsService = serviceGenerator.createService(NewsService.class, Constants.BASE_URL);
|
||||
// ServiceResponse serviceResponse = processCall(newsService.fetchNews(), false);
|
||||
// NewsModel newsModel = (NewsModel) serviceResponse.getData();
|
||||
// subscriber.onNext(newsModel);
|
||||
// subscriber.onCompleted();
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
Observable<NewsModel> newsObservable = Observable.create(newsModelObservableEmitter -> {
|
||||
if (!isConnected(App.getContext())) {
|
||||
Exception e = new NetworkErrorException();
|
||||
newsModelObservableEmitter.onError(e);
|
||||
} else {
|
||||
NewsService newsService = serviceGenerator.createService(NewsService.class, Constants.BASE_URL);
|
||||
ServiceResponse serviceResponse = processCall(newsService.fetchNews(), false);
|
||||
NewsModel newsModel = (NewsModel) serviceResponse.getData();
|
||||
newsModelObservableEmitter.onNext(newsModel);
|
||||
newsModelObservableEmitter.onComplete();
|
||||
}
|
||||
});
|
||||
return newsObservable;
|
||||
|
||||
@@ -10,6 +10,8 @@ public class ServiceError {
|
||||
private static final int GROUP_400 = 4;
|
||||
private static final int GROUP_500 = 5;
|
||||
private static final int VALUE_100 = 100;
|
||||
public static final int SUCCESS_CODE = 200;
|
||||
public static final int ERROR_CODE = 400;
|
||||
private String description;
|
||||
private int code;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
/**
|
||||
@@ -49,7 +49,7 @@ public class ServiceGenerator {
|
||||
retrofit = new Retrofit.Builder()
|
||||
.baseUrl(baseUrl).client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()).build();
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build();
|
||||
return retrofit.create(serviceClass);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package com.task.di;
|
||||
|
||||
|
||||
import com.task.data.local.LocalRepository;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.task.data.local.LocalRepository;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
||||
/**
|
||||
* Created by AhmedEltaher on 5/12/2016
|
||||
@@ -28,4 +29,11 @@ public class MainModule {
|
||||
Gson gson = new GsonBuilder().create();
|
||||
return gson;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public CompositeDisposable provideCompositeSubscription() {
|
||||
CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
return compositeDisposable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.task.ui.component.news;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
|
||||
import com.task.data.remote.dto.NewsItem;
|
||||
import com.task.data.remote.dto.NewsModel;
|
||||
@@ -13,7 +14,7 @@ import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.text.TextUtils.isEmpty;
|
||||
import static com.task.utils.ObjectUtil.isEmpty;
|
||||
import static com.task.utils.ObjectUtil.isNull;
|
||||
|
||||
/**
|
||||
@@ -75,6 +76,11 @@ public class HomePresenter extends Presenter<HomeView> {
|
||||
getView().navigateToDetailsScreen(newsModel.getNewsItems().get(position));
|
||||
};
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
public NewsModel getNewsModel() {
|
||||
return newsModel;
|
||||
}
|
||||
|
||||
private final Callback callback = new Callback() {
|
||||
@Override
|
||||
public void onSuccess(NewsModel newsModel) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.task.usecase;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.task.data.DataRepository;
|
||||
import com.task.data.remote.dto.NewsItem;
|
||||
import com.task.data.remote.dto.NewsModel;
|
||||
@@ -10,32 +8,28 @@ import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import rx.subscriptions.CompositeSubscription;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
||||
|
||||
/**
|
||||
* Created by AhmedEltaher on 5/12/2016
|
||||
*/
|
||||
|
||||
public class NewsUseCase {
|
||||
DataRepository dataRepository;
|
||||
@NonNull
|
||||
private CompositeSubscription mSubscriptions;
|
||||
private DataRepository dataRepository;
|
||||
private CompositeDisposable mSubscriptions;
|
||||
|
||||
@Inject
|
||||
public NewsUseCase(DataRepository dataRepository) {
|
||||
public NewsUseCase(DataRepository dataRepository,CompositeDisposable mSubscriptions) {
|
||||
this.dataRepository = dataRepository;
|
||||
this.mSubscriptions = new CompositeSubscription();
|
||||
this.mSubscriptions = mSubscriptions;
|
||||
}
|
||||
|
||||
public void getNews(final Callback callback) {
|
||||
mSubscriptions.add(dataRepository.requestNews().subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
mSubscriptions.add(dataRepository.requestNews().observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(newsModel -> callback.onSuccess(newsModel),
|
||||
exception -> {
|
||||
callback.onFail();
|
||||
}));
|
||||
exception -> callback.onFail()));
|
||||
}
|
||||
|
||||
public NewsItem searchByTitle(List<NewsItem> news, String keyWord) {
|
||||
@@ -48,7 +42,7 @@ public class NewsUseCase {
|
||||
}
|
||||
|
||||
public void unSubscribe() {
|
||||
mSubscriptions.unsubscribe();
|
||||
mSubscriptions.clear();
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
|
||||
package com.task.ui.component.news;
|
||||
|
||||
import com.task.data.remote.dto.NewsItem;
|
||||
import com.task.data.remote.dto.NewsModel;
|
||||
import com.task.usecase.NewsUseCase;
|
||||
import com.task.usecase.NewsUseCase.Callback;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.core.IsEqual.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class HomePresenterTest {
|
||||
|
||||
@Mock
|
||||
private NewsUseCase newsUseCase;
|
||||
@Mock
|
||||
private NewsModel newsModelMock;
|
||||
@Mock
|
||||
private HomeView homeView;
|
||||
@Mock
|
||||
private Callback callback;
|
||||
@Mock
|
||||
private List<NewsItem> newsItems;
|
||||
@Mock
|
||||
private NewsItem newsItem;
|
||||
|
||||
private HomePresenter homePresenter;
|
||||
private String newsTitle = "this is test";
|
||||
private NewsModel newsModel;
|
||||
private TestModelsGenerator testModelsGenerator;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
testModelsGenerator = new TestModelsGenerator();
|
||||
newsModel = testModelsGenerator.generateNewsModel(newsTitle);
|
||||
doAnswer(invocation -> {
|
||||
((Callback) invocation.getArguments()[0]).onSuccess(newsModel);
|
||||
return null;
|
||||
}).when(newsUseCase).getNews(any(Callback.class));
|
||||
homePresenter = new HomePresenter(newsUseCase);
|
||||
homePresenter.setView(homeView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getNewsList() {
|
||||
// Let's do a synchronous answer for the callback
|
||||
doAnswer(invocation -> {
|
||||
((Callback) invocation.getArguments()[0]).onSuccess(newsModel);
|
||||
return null;
|
||||
}).when(newsUseCase).getNews(any(Callback.class));
|
||||
|
||||
homePresenter.getNews();
|
||||
verify(homeView, times(1)).setLoaderVisibility(true);
|
||||
verify(homeView, times(2)).setNoDataVisibility(false);
|
||||
verify(homeView, times(1)).setListVisibility(false);
|
||||
verify(newsUseCase, times(1)).getNews(any(Callback.class));
|
||||
assertThat(homePresenter.getNewsModel(), is(equalTo(newsModel)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchSuccess() {
|
||||
when(newsUseCase.searchByTitle(newsModel.getNewsItems(), newsTitle)).thenReturn(newsItem);
|
||||
homePresenter.getNews();
|
||||
homePresenter.onSearchClick(newsTitle);
|
||||
verify(homeView, times(1)).navigateToDetailsScreen(any(NewsItem.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchFailedWhileEmptyList() {
|
||||
homePresenter.getNews();
|
||||
homePresenter.onSearchClick(newsTitle);
|
||||
assertThat(newsModelMock.getNewsItems().size(), equalTo(0));
|
||||
verify(homeView, times(1)).showSearchError();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchFailedWhenNothingMatches() {
|
||||
when(newsUseCase.searchByTitle(any(), any())).thenReturn(null);
|
||||
homePresenter.getNews();
|
||||
homePresenter.onSearchClick(newsTitle);
|
||||
verify(homeView, times(1)).showSearchError();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.task.ui.component.news;
|
||||
|
||||
import com.task.data.remote.ServiceResponse;
|
||||
import com.task.data.remote.dto.NewsItem;
|
||||
import com.task.data.remote.dto.NewsModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.task.data.remote.ServiceError.ERROR_CODE;
|
||||
import static com.task.data.remote.ServiceError.SUCCESS_CODE;
|
||||
|
||||
/**
|
||||
* Created by ahmedeltaher on 3/8/17.
|
||||
*/
|
||||
|
||||
public class TestModelsGenerator {
|
||||
|
||||
public NewsModel generateNewsModel(String stup) {
|
||||
NewsModel newsModel = new NewsModel();
|
||||
newsModel.setCopyright(stup);
|
||||
newsModel.setLastUpdated(stup);
|
||||
newsModel.setSection(stup);
|
||||
newsModel.setStatus(stup);
|
||||
newsModel.setNumResults(25L);
|
||||
List<NewsItem> newsItems = new ArrayList<>();
|
||||
for (int i = 0; i < 25; i++) {
|
||||
newsItems.add(generateNewsItemModel(stup));
|
||||
}
|
||||
newsModel.setNewsItems(newsItems);
|
||||
return newsModel;
|
||||
}
|
||||
|
||||
public NewsItem generateNewsItemModel(String stup) {
|
||||
NewsItem newsItem = new NewsItem();
|
||||
newsItem.setTitle(stup);
|
||||
newsItem.setAbstract(stup);
|
||||
newsItem.setUrl(stup);
|
||||
return newsItem;
|
||||
}
|
||||
|
||||
public ServiceResponse getNewsSuccessfulModel() {
|
||||
String stupString = "this is temp string";
|
||||
NewsModel newsModel = generateNewsModel(stupString);
|
||||
return new ServiceResponse(SUCCESS_CODE, newsModel);
|
||||
}
|
||||
|
||||
public ServiceResponse getNewsErrorModel() {
|
||||
return new ServiceResponse(ERROR_CODE, null);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.2.3'
|
||||
//DI
|
||||
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
|
||||
classpath 'com.android.tools.build:gradle:2.3.0'
|
||||
//lamda
|
||||
classpath 'me.tatarka:gradle-retrolambda:3.2.5'
|
||||
classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Mon Dec 28 10:00:20 PST 2015
|
||||
#Fri Mar 10 17:20:13 CET 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
|
||||
|
||||
Reference in New Issue
Block a user