├── .gitignore ├── README.md ├── build.gradle ├── cityscoremvp ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── co │ │ └── netguru │ │ └── android │ │ └── cityscoremvp │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── co │ │ │ └── netguru │ │ │ └── android │ │ │ └── cityscoremvp │ │ │ ├── App.java │ │ │ ├── MainActivity.java │ │ │ ├── Utils.java │ │ │ ├── data │ │ │ ├── City.java │ │ │ ├── CityQuality.java │ │ │ └── quality │ │ │ │ ├── CityApi.java │ │ │ │ └── CityController.java │ │ │ ├── di │ │ │ ├── ActivityScope.java │ │ │ ├── ApplicationComponent.java │ │ │ ├── ApplicationModule.java │ │ │ ├── FragmentScope.java │ │ │ └── NetworkModule.java │ │ │ └── search │ │ │ ├── CityQualityAdapter.java │ │ │ ├── CityQualityViewHolder.java │ │ │ ├── SearchComponent.java │ │ │ ├── SearchContract.java │ │ │ ├── SearchFragment.java │ │ │ └── SearchPresenter.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_search.xml │ │ └── item_city_quality.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── co │ └── netguru │ └── android │ └── cityscoremvp │ └── ExampleUnitTest.java ├── cityscoremvvm ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── co │ │ └── netguru │ │ └── android │ │ └── io17 │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── co │ │ │ └── netguru │ │ │ └── android │ │ │ └── io17 │ │ │ ├── App.java │ │ │ ├── MainActivity.java │ │ │ ├── MyObserver.java │ │ │ ├── Utils.java │ │ │ ├── data │ │ │ ├── City.java │ │ │ ├── CityQuality.java │ │ │ └── quality │ │ │ │ ├── CityApi.java │ │ │ │ ├── CityRepository.java │ │ │ │ └── CityRepositoryImpl.java │ │ │ ├── di │ │ │ ├── ActivityScope.java │ │ │ ├── ApplicationComponent.java │ │ │ ├── ApplicationModule.java │ │ │ └── NetworkModule.java │ │ │ └── search │ │ │ ├── CityQualityAdapter.java │ │ │ ├── CityQualityViewHolder.java │ │ │ ├── SearchComponent.java │ │ │ ├── SearchFragment.java │ │ │ ├── SearchViewModel.java │ │ │ └── SearchViewModelFactory.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_search.xml │ │ └── item_city_quality.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── co │ └── netguru │ └── android │ └── io17 │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Application showcasing differences between MVP (with rxJava) and MVVM (with new android components from I/O 17 - LiveData, ViewModel but also DataBinding) 2 | 3 | * Fetching data from API using Retrofit 4 | * Handling configuration changes in both MVP and MVVM 5 | * Injecting ViewModel with Dagger 6 | * DataBinding usage with ViewModel 7 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | maven { url 'https://jitpack.io' } 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:2.3.0' 11 | classpath 'me.tatarka:gradle-retrolambda:3.6.1' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | jcenter() 21 | mavenCentral() 22 | maven { 23 | url "https://maven.google.com" 24 | } 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /cityscoremvp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /cityscoremvp/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'me.tatarka.retrolambda' 3 | 4 | android { 5 | compileSdkVersion 25 6 | buildToolsVersion "25.0.2" 7 | 8 | defaultConfig { 9 | applicationId "co.netguru.android.cityscoremvp" 10 | minSdkVersion 19 11 | targetSdkVersion 25 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 16 | 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | dependencies { 31 | compile fileTree(dir: 'libs', include: ['*.jar']) 32 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 33 | exclude group: 'com.android.support', module: 'support-annotations' 34 | }) 35 | 36 | compile 'com.android.support:appcompat-v7:25.3.1' 37 | compile 'com.android.support:recyclerview-v7:25.3.1' 38 | compile 'com.squareup.retrofit2:retrofit:2.3.0' 39 | compile 'com.google.dagger:dagger:2.4' 40 | compile 'com.jakewharton:butterknife:8.6.0' 41 | compile 'io.reactivex:rxandroid:1.2.1' 42 | compile 'io.reactivex:rxjava:1.1.6' 43 | compile 'com.google.code.gson:gson:2.8.0' 44 | compile 'com.squareup.retrofit2:converter-gson:2.3.0' 45 | compile 'com.squareup.retrofit2:adapter-rxjava:2.3.0' 46 | compile 'com.hannesdorfmann.mosby3:mvp-lce:3.0.4' 47 | compile 'com.android.support.constraint:constraint-layout:1.0.0-beta4' 48 | testCompile 'junit:junit:4.12' 49 | annotationProcessor 'com.google.dagger:dagger-compiler:2.4' 50 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0' 51 | } 52 | -------------------------------------------------------------------------------- /cityscoremvp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/filip/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /cityscoremvp/src/androidTest/java/co/netguru/android/cityscoremvp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("co.netguru.android.cityscoremvp", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/App.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import co.netguru.android.cityscoremvp.di.ApplicationComponent; 7 | import co.netguru.android.cityscoremvp.di.ApplicationModule; 8 | import co.netguru.android.cityscoremvp.di.DaggerApplicationComponent; 9 | 10 | public class App extends Application { 11 | 12 | private ApplicationComponent appComponent; 13 | 14 | public static ApplicationComponent getAppComponent(Context context) { 15 | return ((App) context.getApplicationContext()).appComponent; 16 | } 17 | 18 | @Override 19 | protected void attachBaseContext(Context base) { 20 | super.attachBaseContext(base); 21 | 22 | // init dagger appComponent 23 | this.appComponent = DaggerApplicationComponent 24 | .builder() 25 | .applicationModule(new ApplicationModule(this)) 26 | .build(); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/MainActivity.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import co.netguru.android.cityscoremvp.R; 7 | import co.netguru.android.cityscoremvp.search.SearchFragment; 8 | 9 | public class MainActivity extends AppCompatActivity { 10 | 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.activity_main); 15 | 16 | if (savedInstanceState == null) { 17 | SearchFragment searchFragment = new SearchFragment(); 18 | getSupportFragmentManager() 19 | .beginTransaction() 20 | .add(R.id.container_search, searchFragment) 21 | .commit(); 22 | } 23 | 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/Utils.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp; 2 | 3 | import android.text.Html; 4 | import android.text.Spanned; 5 | 6 | public final class Utils { 7 | 8 | private Utils() {} 9 | 10 | public static Spanned stripHtml(String html) { 11 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { 12 | return Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY); 13 | } else { 14 | return Html.fromHtml(html); 15 | } 16 | } 17 | 18 | public static String capitalize(String text) { 19 | return text.substring(0,1).toUpperCase() + text.substring(1); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/data/City.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.data; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.List; 6 | 7 | public class City { 8 | private String name; 9 | 10 | @SerializedName("summary") 11 | private String description; 12 | @SerializedName("teleport_city_score") 13 | private float score; 14 | @SerializedName("categories") 15 | private List qualities; 16 | 17 | public City(String name, String description, float score, List qualities) { 18 | this.name = name; 19 | this.description = description; 20 | this.score = score; 21 | this.qualities = qualities; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public void setName(String name) { 29 | this.name = name; 30 | } 31 | 32 | public String getDescription() { 33 | return description; 34 | } 35 | 36 | public void setDescription(String description) { 37 | this.description = description; 38 | } 39 | 40 | public float getScore() { 41 | return score; 42 | } 43 | 44 | public void setScore(float score) { 45 | this.score = score; 46 | } 47 | 48 | public List getQualities() { 49 | return qualities; 50 | } 51 | 52 | public void setQualities(List qualities) { 53 | this.qualities = qualities; 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return "City{" + 59 | "name='" + name + '\'' + 60 | ", description='" + description + '\'' + 61 | ", score=" + score + 62 | ", qualities=" + qualities + 63 | '}'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/data/CityQuality.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.data; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class CityQuality { 6 | private String name; 7 | @SerializedName("score_out_of_10") 8 | private float score; 9 | 10 | public CityQuality(String name, float score) { 11 | this.name = name; 12 | this.score = score; 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | public void setName(String name) { 20 | this.name = name; 21 | } 22 | 23 | public float getScore() { 24 | return score; 25 | } 26 | 27 | public void setScore(float score) { 28 | this.score = score; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/data/quality/CityApi.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.data.quality; 2 | 3 | import co.netguru.android.cityscoremvp.data.City; 4 | import retrofit2.http.GET; 5 | import retrofit2.http.Path; 6 | import rx.Observable; 7 | 8 | public interface CityApi { 9 | @GET("urban_areas/slug:{city}/scores") 10 | Observable getCityInformation(@Path("city") String city); 11 | } 12 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/data/quality/CityController.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.data.quality; 2 | 3 | import android.util.Log; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import javax.inject.Inject; 8 | import javax.inject.Singleton; 9 | 10 | import co.netguru.android.cityscoremvp.data.City; 11 | import rx.Observable; 12 | 13 | import static co.netguru.android.cityscoremvp.Utils.capitalize; 14 | 15 | @Singleton 16 | public class CityController { 17 | 18 | private final CityApi cityApi; 19 | 20 | @Inject 21 | public CityController(CityApi cityApi) { 22 | this.cityApi = cityApi; 23 | } 24 | 25 | public Observable getCityByName(String name) { 26 | String transformedName = transformCityName(name); 27 | return cityApi.getCityInformation(transformedName) 28 | .cache() 29 | .map(city -> { 30 | city.setName(capitalize(name)); 31 | return city; 32 | }); 33 | } 34 | 35 | private String transformCityName(String name) { 36 | return name.toLowerCase().replace(" ", "-"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/di/ActivityScope.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.di; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Scope; 7 | 8 | @Scope 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface ActivityScope { 11 | } 12 | 13 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/di/ApplicationComponent.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.di; 2 | 3 | import javax.inject.Singleton; 4 | 5 | import co.netguru.android.cityscoremvp.search.SearchComponent; 6 | import dagger.Component; 7 | 8 | @Singleton 9 | @Component(modules = {ApplicationModule.class, NetworkModule.class}) 10 | public interface ApplicationComponent { 11 | 12 | SearchComponent plusSearchComponent(); 13 | } 14 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/di/ApplicationModule.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.di; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import dagger.Module; 9 | import dagger.Provides; 10 | 11 | @Module 12 | public class ApplicationModule { 13 | 14 | private Application application; 15 | 16 | public ApplicationModule(Application application) { 17 | this.application = application; 18 | } 19 | 20 | @Provides 21 | @Singleton 22 | Context provideContext() { 23 | return application; 24 | } 25 | 26 | @Provides 27 | @Singleton 28 | Application provideApplication() { 29 | return application; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/di/FragmentScope.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.di; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | import javax.inject.Scope; 7 | 8 | @Scope 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface FragmentScope { 11 | } 12 | 13 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/di/NetworkModule.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.di; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | 6 | import javax.inject.Singleton; 7 | 8 | import co.netguru.android.cityscoremvp.data.quality.CityApi; 9 | import dagger.Module; 10 | import dagger.Provides; 11 | import retrofit2.Retrofit; 12 | import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; 13 | import retrofit2.converter.gson.GsonConverterFactory; 14 | 15 | @Module 16 | public class NetworkModule { 17 | 18 | @Provides 19 | @Singleton 20 | Gson provideGson() { 21 | return new GsonBuilder().create(); 22 | } 23 | 24 | @Provides 25 | @Singleton 26 | Retrofit provideRetrofit(Gson gson) { 27 | return new Retrofit.Builder() 28 | .addConverterFactory(GsonConverterFactory.create(gson)) 29 | .baseUrl("https://api.teleport.org/api/") 30 | .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) 31 | .build(); 32 | } 33 | 34 | @Provides 35 | @Singleton 36 | CityApi provideCityApi(Retrofit retrofit) { 37 | return retrofit.create(CityApi.class); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/search/CityQualityAdapter.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.search; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.ViewGroup; 5 | 6 | import co.netguru.android.cityscoremvp.data.City; 7 | 8 | public class CityQualityAdapter extends RecyclerView.Adapter { 9 | 10 | private City city; 11 | 12 | @Override 13 | public CityQualityViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 14 | return new CityQualityViewHolder(parent); 15 | } 16 | 17 | @Override 18 | public void onBindViewHolder(CityQualityViewHolder holder, int position) { 19 | holder.bind(city.getQualities().get(position)); 20 | } 21 | 22 | @Override 23 | public int getItemCount() { 24 | return city.getQualities().size(); 25 | } 26 | 27 | public void setCity(City city) { 28 | this.city = city; 29 | notifyItemRangeChanged(0, city.getQualities().size()); 30 | } 31 | 32 | public City getData() { 33 | return city; 34 | } 35 | } -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/search/CityQualityViewHolder.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.search; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.TextView; 8 | 9 | import butterknife.BindView; 10 | import butterknife.ButterKnife; 11 | import co.netguru.android.cityscoremvp.data.CityQuality; 12 | import co.netguru.android.cityscoremvp.R; 13 | 14 | public class CityQualityViewHolder extends RecyclerView.ViewHolder { 15 | 16 | @BindView(R.id.city_quality) 17 | TextView qualityName; 18 | 19 | public CityQualityViewHolder(ViewGroup parent) { 20 | super(LayoutInflater.from(parent.getContext()) 21 | .inflate(R.layout.item_city_quality, parent, false)); 22 | 23 | ButterKnife.bind(this, itemView); 24 | } 25 | 26 | public void bind(CityQuality cityQuality) { 27 | String text = cityQuality.getName() + ": " + String.format("%.1f", cityQuality.getScore()); 28 | qualityName.setText(text); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/search/SearchComponent.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.search; 2 | 3 | import co.netguru.android.cityscoremvp.di.FragmentScope; 4 | import dagger.Subcomponent; 5 | 6 | @FragmentScope 7 | @Subcomponent 8 | public interface SearchComponent { 9 | SearchPresenter getPresenter(); 10 | } 11 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/search/SearchContract.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.search; 2 | 3 | import com.hannesdorfmann.mosby3.mvp.MvpPresenter; 4 | import com.hannesdorfmann.mosby3.mvp.lce.MvpLceView; 5 | 6 | import co.netguru.android.cityscoremvp.data.City; 7 | 8 | public class SearchContract { 9 | 10 | interface View extends MvpLceView { 11 | 12 | } 13 | 14 | interface Presenter extends MvpPresenter { 15 | void onSearch(String cityName); 16 | } 17 | } -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/search/SearchFragment.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.search; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | import android.support.v7.widget.GridLayoutManager; 8 | import android.support.v7.widget.RecyclerView; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | import android.view.inputmethod.InputMethodManager; 13 | import android.widget.EditText; 14 | import android.widget.ProgressBar; 15 | import android.widget.RelativeLayout; 16 | import android.widget.TextView; 17 | 18 | import com.hannesdorfmann.mosby3.mvp.viewstate.lce.LceViewState; 19 | import com.hannesdorfmann.mosby3.mvp.viewstate.lce.MvpLceViewStateFragment; 20 | import com.hannesdorfmann.mosby3.mvp.viewstate.lce.data.RetainingLceViewState; 21 | 22 | import butterknife.BindView; 23 | import butterknife.ButterKnife; 24 | import butterknife.OnClick; 25 | import butterknife.Unbinder; 26 | import co.netguru.android.cityscoremvp.App; 27 | import co.netguru.android.cityscoremvp.R; 28 | import co.netguru.android.cityscoremvp.Utils; 29 | import co.netguru.android.cityscoremvp.data.City; 30 | 31 | public class SearchFragment extends MvpLceViewStateFragment 33 | implements SearchContract.View { 34 | 35 | private final CityQualityAdapter adapter = new CityQualityAdapter(); 36 | 37 | @BindView(R.id.search_field) 38 | EditText searchField; 39 | 40 | @BindView(R.id.contentView) 41 | RelativeLayout resultLayout; 42 | 43 | @BindView(R.id.loadingView) 44 | ProgressBar progressBar; 45 | 46 | @BindView(R.id.recycler_view) 47 | RecyclerView recyclerView; 48 | 49 | @BindView(R.id.city_name) 50 | TextView cityNameTextView; 51 | 52 | @BindView(R.id.city_score) 53 | TextView cityScoreTextView; 54 | 55 | @BindView(R.id.city_description) 56 | TextView cityDescriptionTextView; 57 | 58 | private Unbinder unbinder; 59 | 60 | @OnClick(R.id.search_button) 61 | void onSearchClick() { 62 | getPresenter().onSearch(searchField.getText().toString()); 63 | hideKeyboard(); 64 | } 65 | 66 | @Nullable 67 | @Override 68 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 69 | return inflater.inflate(R.layout.fragment_search, container, false); 70 | } 71 | 72 | @Override 73 | public void onCreate(Bundle savedInstanceState) { 74 | super.onCreate(savedInstanceState); 75 | setRetainInstance(true); 76 | } 77 | 78 | @Override 79 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 80 | super.onViewCreated(view, savedInstanceState); 81 | unbinder = ButterKnife.bind(this, view); 82 | initRecycler(); 83 | } 84 | 85 | @Override 86 | public void onDestroyView() { 87 | super.onDestroyView(); 88 | unbinder.unbind(); 89 | } 90 | 91 | @Override 92 | public SearchContract.Presenter createPresenter() { 93 | return App.getAppComponent(getActivity()).plusSearchComponent().getPresenter(); 94 | } 95 | 96 | @Override 97 | public void showLoading(boolean show) { 98 | progressBar.setVisibility(show ? View.VISIBLE : View.GONE); 99 | } 100 | 101 | @Override 102 | public City getData() { 103 | return adapter.getData(); 104 | } 105 | 106 | @Override 107 | public void setData(City city) { 108 | cityNameTextView.setText(city.getName()); 109 | cityScoreTextView.setText("City score: " + String.format("%.02f", city.getScore())); 110 | cityDescriptionTextView.setText(Utils.stripHtml(city.getDescription())); 111 | adapter.setCity(city); 112 | showContent(); 113 | } 114 | 115 | @Override 116 | public void loadData(boolean pullToRefresh) { 117 | // no-op 118 | } 119 | 120 | @NonNull 121 | @Override 122 | public LceViewState createViewState() { 123 | return new RetainingLceViewState<>(); 124 | } 125 | 126 | @Override 127 | protected String getErrorMessage(Throwable e, boolean pullToRefresh) { 128 | return e.getLocalizedMessage(); 129 | } 130 | 131 | private void initRecycler() { 132 | recyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); 133 | recyclerView.setAdapter(adapter); 134 | } 135 | 136 | private void hideKeyboard() { 137 | InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 138 | imm.hideSoftInputFromWindow(searchField.getWindowToken(), 0); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/java/co/netguru/android/cityscoremvp/search/SearchPresenter.java: -------------------------------------------------------------------------------- 1 | package co.netguru.android.cityscoremvp.search; 2 | 3 | import com.hannesdorfmann.mosby3.mvp.MvpBasePresenter; 4 | 5 | import javax.inject.Inject; 6 | 7 | import co.netguru.android.cityscoremvp.data.quality.CityController; 8 | import co.netguru.android.cityscoremvp.di.FragmentScope; 9 | import retrofit2.HttpException; 10 | import rx.android.schedulers.AndroidSchedulers; 11 | import rx.schedulers.Schedulers; 12 | import rx.subscriptions.CompositeSubscription; 13 | 14 | @FragmentScope 15 | public class SearchPresenter extends MvpBasePresenter 16 | implements SearchContract.Presenter { 17 | 18 | private final CityController cityController; 19 | private final CompositeSubscription subscriptions = new CompositeSubscription(); 20 | 21 | @Inject 22 | SearchPresenter(CityController cityController) { 23 | this.cityController = cityController; 24 | } 25 | 26 | @Override 27 | public void attachView(SearchContract.View view) { 28 | super.attachView(view); 29 | } 30 | 31 | @Override 32 | public void onSearch(String cityName) { 33 | getView().showLoading(false); 34 | 35 | subscriptions.add(cityController.getCityByName(cityName) 36 | .subscribeOn(Schedulers.io()) 37 | .observeOn(AndroidSchedulers.mainThread()) 38 | .subscribe(getView()::setData, this::handleError)); 39 | } 40 | 41 | @Override 42 | public void detachView(boolean retainInstance) { 43 | super.detachView(retainInstance); 44 | subscriptions.clear(); 45 | } 46 | 47 | private void handleError(Throwable throwable) { 48 | String message; 49 | if (throwable instanceof HttpException) { 50 | HttpException httpException = (HttpException) throwable; 51 | if (httpException.code() == 404) { 52 | 53 | message = "Could not find that city"; 54 | } else { 55 | message = "Unknown error"; 56 | } 57 | } else { 58 | message = "Unknown error"; 59 | } 60 | 61 | getView().showError(new Throwable(message, throwable), false); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /cityscoremvp/src/main/res/layout/fragment_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 |