├── .gitignore ├── .travis.yml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── lib │ ├── espresso-1.1.jar │ ├── testrunner-1.1.jar │ └── testrunner-runtime-1.1.jar ├── my-release-key.keystore ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── dp │ │ │ └── weather │ │ │ └── app │ │ │ ├── AppComponent.kt │ │ │ ├── AppModule.kt │ │ │ ├── BusModule.kt │ │ │ ├── BusSubcomponent.kt │ │ │ ├── Const.kt │ │ │ ├── SchedulersManager.kt │ │ │ ├── WeatherApp.kt │ │ │ ├── activity │ │ │ ├── ActivityComponent.kt │ │ │ ├── ActivityModule.kt │ │ │ ├── BaseActivity.kt │ │ │ ├── BaseActivityComponent.kt │ │ │ ├── HasComponent.kt │ │ │ ├── MainActivity.kt │ │ │ ├── SettingsActivity.kt │ │ │ └── debug │ │ │ │ ├── DebugActivity.kt │ │ │ │ ├── DebugBusModule.kt │ │ │ │ └── DebugBusSubcomponent.kt │ │ │ ├── adapter │ │ │ ├── CursorRecyclerViewAdapter.kt │ │ │ ├── OrmliteCursorRecyclerViewAdapter.kt │ │ │ ├── PlacesAdapter.kt │ │ │ └── PlacesAutoCompleteAdapter.kt │ │ │ ├── annotation │ │ │ ├── CachePrefs.kt │ │ │ ├── ConfigPrefs.kt │ │ │ ├── IOSched.kt │ │ │ ├── PerActivity.kt │ │ │ ├── PerFragment.kt │ │ │ └── UISched.kt │ │ │ ├── db │ │ │ ├── DatabaseHelper.kt │ │ │ ├── NoIdCursorWrapper.kt │ │ │ ├── OrmliteCursorAdapter.kt │ │ │ ├── OrmliteCursorLoader.kt │ │ │ ├── OrmliteListLoader.kt │ │ │ ├── Queries.kt │ │ │ ├── Tables.kt │ │ │ └── table │ │ │ │ └── Place.kt │ │ │ ├── event │ │ │ ├── AddPlaceEvent.kt │ │ │ ├── DeletePlaceEvent.kt │ │ │ └── UpdateListEvent.kt │ │ │ ├── fragment │ │ │ ├── BaseFragment.kt │ │ │ └── WeatherFragment.kt │ │ │ ├── net │ │ │ ├── PlacesApi.kt │ │ │ ├── WeatherApi.kt │ │ │ └── dto │ │ │ │ ├── CurrentCondition.kt │ │ │ │ ├── Data.kt │ │ │ │ ├── Forecast.kt │ │ │ │ ├── Request.kt │ │ │ │ ├── Weather.kt │ │ │ │ ├── WeatherDesc.kt │ │ │ │ └── WeatherIconUrl.kt │ │ │ ├── utils │ │ │ ├── AsyncBus.kt │ │ │ ├── MetricsController.kt │ │ │ ├── MyPreferences.kt │ │ │ ├── Observables.kt │ │ │ └── WhiteBorderCircleTransformation.kt │ │ │ └── widget │ │ │ ├── ArrayAdapterSearchView.kt │ │ │ └── WeatherFor5DaysView.kt │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_launcher.png │ │ └── ico_menu_small.png │ │ ├── drawable-mdpi │ │ ├── ic_launcher.png │ │ └── ico_menu_small.png │ │ ├── drawable-xhdpi │ │ ├── ic_launcher.png │ │ └── ico_menu_small.png │ │ ├── drawable-xxhdpi │ │ ├── ic_launcher.png │ │ └── ico_menu_small.png │ │ ├── drawable-xxxhdpi │ │ └── ic_launcher.png │ │ ├── drawable │ │ └── card.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── activity_test.xml │ │ ├── fragment_main.xml │ │ ├── fragment_weather.xml │ │ ├── item_city_weather.xml │ │ ├── item_place_content.xml │ │ ├── item_search_list.xml │ │ └── view_weather_for_week.xml │ │ ├── menu │ │ ├── item_place.xml │ │ ├── main.xml │ │ └── menu_debug.xml │ │ ├── raw │ │ └── ormlite_config.txt │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── strings_activity_settings.xml │ │ └── styles.xml │ │ └── xml │ │ └── pref_general.xml │ └── test │ ├── TestAndroidManifest.xml │ └── java │ └── io │ └── dp │ └── weather │ └── app │ ├── MockAppComponent.kt │ ├── MockAppModule.kt │ ├── TestApp.kt │ ├── TestUtils.kt │ ├── activity │ └── MockActivity.kt │ └── fragment │ └── WeatherFragmentTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | /build 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | android: 7 | components: 8 | - build-tools-23.0.1 9 | - android-23 10 | - extra-android-m2repository 11 | 12 | env: 13 | global: 14 | - ADB_INSTALL_TIMEOUT=8 15 | 16 | script: 17 | - ./gradlew clean build -x test 18 | 19 | branches: 20 | except: 21 | - gh-pages 22 | 23 | notifications: 24 | email: false 25 | 26 | sudo: false 27 | 28 | cache: 29 | directories: 30 | - $HOME/.gradle -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### What is this repository for? ### 2 | 3 | * This is weather android app which uses worldweatheronline.com as data provider 4 | * v1.0.0 5 | 6 | ### How do I get set up? ### 7 | 8 | * ./gradlew clean build 9 | * and don't forget to setup your sdk.dir (via local.properties or direct specification) 10 | 11 | ### Design concepts ### 12 | 13 | The whole design of my app is based on Dependency Injections and EventBus concepts. 14 | I wanted to split all classes out from each other onto independent components. 15 | 16 | Interaction with weather web-service is based on fetching forecast by latitude and longitude. 17 | I use shared preferences as a cache for forecast responses and update this cache in two cases: 18 | 19 | 1. user refreshed the list manually by pull-to-refresh action on the list of data 20 | 2. time of response is expired (it's 24 hours) and in the next scrolling action - application will try to fetch new data for this location 21 | 22 | ### Dependencies ### 23 | I used a lot of 3rd libraries and the main reason why I did it, just to speed up development process. 24 | 25 | * rxjava - don't like to hassle with AsyncTask 26 | * retrofit - quick bootstrap of any http webservice stub 27 | * ormlite - quick bootstrap for database 28 | * otto - eventbus 29 | * dagger2 - DI 30 | * com.etsy.android.grid - Staggered GridView for weather dashboard 31 | * timber - very customizable logger for android 32 | 33 | ### Which design patterns I used ### 34 | DI, EventBus, Observer, Builder 35 | 36 | Applications consists of two screens: 37 | 38 | 1. main screen with weather dashboard 39 | 2. settings activity where the user can specify Temperature Units (Celcius or Fahrenheit) 40 | 3. and Debug activity to show how to override eventbus 41 | 42 | Small clarification: To refresh weathers list - just pull down (I used SwipeRefreshLayout from support library) 43 | 44 | ### Unit-testing ### 45 | To run & debug unit-tests in IDE you need to use Android Studio 1.4 46 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.0.0-beta-2423' 3 | repositories { 4 | jcenter() 5 | 6 | maven { 7 | url 'http://oss.sonatype.org/content/repositories/snapshots' 8 | } 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:1.3.1' 13 | 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" 16 | } 17 | } 18 | 19 | apply plugin: 'com.android.application' 20 | apply plugin: 'kotlin-android' 21 | 22 | android { 23 | compileSdkVersion 23 24 | buildToolsVersion "23.0.1" 25 | 26 | defaultConfig { 27 | applicationId "io.dp.weather.app" 28 | minSdkVersion 14 29 | targetSdkVersion 23 30 | versionCode 1 31 | versionName "1.0" 32 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 33 | 34 | buildConfigField "int", "DATABASE_VERSION", "1"; 35 | buildConfigField "String", "FORECAST_API_URL", 36 | "\"https://api.worldweatheronline.com/free/v1/\"" 37 | buildConfigField "String", "FORECAST_API_KEY", 38 | "\"499cb12ae4fc7740adee2fa7b2007982f75338c0\"" 39 | buildConfigField "String", "PLACES_API_URL", "\"https://maps.googleapis.com/maps/api/\"" 40 | buildConfigField "String", "PLACES_API_KEY", "\"AIzaSyA0o8WdYc40tWfRrKimdAWHvaFwW8h-N8w\"" 41 | } 42 | 43 | signingConfigs { 44 | release { 45 | storeFile file("my-release-key.keystore") 46 | storePassword "12345678" 47 | keyAlias "alias_name" 48 | keyPassword "12345678" 49 | } 50 | } 51 | 52 | buildTypes { 53 | release { 54 | signingConfig signingConfigs.release 55 | } 56 | } 57 | 58 | packagingOptions { 59 | exclude 'META-INF/services/javax.annotation.processing.Processor' 60 | } 61 | 62 | lintOptions { 63 | abortOnError false 64 | } 65 | 66 | testOptions { 67 | unitTests.returnDefaultValues = true 68 | } 69 | 70 | sourceSets { 71 | main.java.srcDirs += 'src/main/kotlin' 72 | test.java.srcDirs += 'src/test/java' 73 | test.java.srcDirs += 'src/test/kotlin' 74 | } 75 | } 76 | 77 | dependencies { 78 | compile fileTree(dir: 'libs', include: ['*.jar']) 79 | 80 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 81 | 82 | compile 'com.android.support:appcompat-v7:23.1.0' 83 | compile 'com.android.support:recyclerview-v7:23.1.0' 84 | 85 | compile 'com.squareup.retrofit:retrofit:1.9.0' 86 | compile 'com.google.dagger:dagger:2.0.2' 87 | kapt 'com.google.dagger:dagger-compiler:2.0.2' 88 | provided 'org.glassfish:javax.annotation:10.0-b28' 89 | 90 | compile 'com.squareup.picasso:picasso:2.5.2' 91 | 92 | compile 'com.squareup:otto:1.3.8' 93 | compile 'com.jakewharton.timber:timber:2.4.1' 94 | compile 'com.j256.ormlite:ormlite-android:4.48' 95 | 96 | compile 'io.reactivex:rxkotlin:0.24.100' 97 | compile 'io.reactivex:rxandroid:1.0.1' 98 | compile 'com.trello:rxlifecycle:0.3.0' 99 | compile 'com.trello:rxlifecycle-components:0.3.0' 100 | 101 | compile 'org.jetbrains.anko:anko-sdk15:0.7.3' 102 | compile 'org.jetbrains.anko:anko-support-v4:0.7.3' 103 | 104 | kaptTest 'com.google.dagger:dagger-compiler:2.0.2' 105 | testCompile 'junit:junit:4.11' 106 | testCompile 'org.glassfish:javax.annotation:10.0-b28' 107 | testCompile 'org.hamcrest:hamcrest-integration:1.1' 108 | testCompile 'org.hamcrest:hamcrest-core:1.1' 109 | testCompile 'org.hamcrest:hamcrest-library:1.1' 110 | testCompile 'com.squareup.retrofit:retrofit-mock:1.9.0' 111 | testCompile 'org.mockito:mockito-all:1.9.5' 112 | testCompile 'org.robolectric:robolectric:3.0' 113 | testCompile 'org.robolectric:shadows-support-v4:3.0' 114 | } 115 | 116 | kapt { 117 | generateStubs = true 118 | } 119 | -------------------------------------------------------------------------------- /app/lib/espresso-1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/lib/espresso-1.1.jar -------------------------------------------------------------------------------- /app/lib/testrunner-1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/lib/testrunner-1.1.jar -------------------------------------------------------------------------------- /app/lib/testrunner-runtime-1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/lib/testrunner-runtime-1.1.jar -------------------------------------------------------------------------------- /app/my-release-key.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/my-release-key.keystore -------------------------------------------------------------------------------- /app/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 /Applications/Android Studio.app/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 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app 2 | 3 | import android.location.Geocoder 4 | import com.google.gson.Gson 5 | import dagger.Component 6 | import io.dp.weather.app.annotation.IOSched 7 | import io.dp.weather.app.annotation.UISched 8 | import io.dp.weather.app.net.PlacesApi 9 | import io.dp.weather.app.net.WeatherApi 10 | import rx.Scheduler 11 | import javax.inject.Singleton 12 | 13 | /** 14 | * Created by deepol on 19/08/15. 15 | */ 16 | @Singleton 17 | @Component(modules = arrayOf(AppModule::class)) 18 | public interface AppComponent { 19 | 20 | public fun provideGson(): Gson 21 | 22 | public fun proviceGeocoder(): Geocoder 23 | 24 | public fun provideForecastApi(): WeatherApi 25 | 26 | public fun providePlacesApi(): PlacesApi 27 | 28 | @IOSched public fun provideIoScheduler(): Scheduler 29 | 30 | @UISched public fun provideUiScheduler(): Scheduler 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/AppModule.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app 2 | 3 | import android.app.Application 4 | import android.location.Geocoder 5 | import com.google.gson.Gson 6 | import com.google.gson.GsonBuilder 7 | import dagger.Module 8 | import dagger.Provides 9 | import io.dp.weather.app.annotation.IOSched 10 | import io.dp.weather.app.annotation.UISched 11 | import io.dp.weather.app.net.PlacesApi 12 | import io.dp.weather.app.net.WeatherApi 13 | import retrofit.RestAdapter 14 | import rx.Scheduler 15 | import rx.android.schedulers.AndroidSchedulers 16 | import rx.schedulers.Schedulers 17 | import timber.log.Timber 18 | import javax.inject.Singleton 19 | 20 | /** 21 | * Created by dp on 07/10/14. 22 | */ 23 | @Module public class AppModule(application: WeatherApp) { 24 | 25 | protected val application: Application 26 | 27 | init { 28 | this.application = application 29 | 30 | Timber.plant(Timber.DebugTree()) 31 | } 32 | 33 | @Provides @Singleton public fun proviceGeocoder(): Geocoder { 34 | return Geocoder(application) 35 | } 36 | 37 | @Provides @Singleton public fun provideApplication(): Application { 38 | return application 39 | } 40 | 41 | @Provides @Singleton public fun provideForecastApi(): WeatherApi { 42 | val b = RestAdapter.Builder() 43 | 44 | if (BuildConfig.DEBUG) { 45 | b.setLogLevel(RestAdapter.LogLevel.FULL) 46 | } 47 | 48 | b.setRequestInterceptor({ request -> 49 | request.addQueryParam("key", BuildConfig.FORECAST_API_KEY) 50 | request.addQueryParam("format", "json") 51 | }) 52 | 53 | val restAdapter = b.setEndpoint(BuildConfig.FORECAST_API_URL).build() 54 | return restAdapter.create(WeatherApi::class.java) 55 | } 56 | 57 | @Provides @Singleton public fun providePlacesApi(): PlacesApi { 58 | val b = RestAdapter.Builder() 59 | 60 | if (BuildConfig.DEBUG) { 61 | b.setLogLevel(RestAdapter.LogLevel.FULL) 62 | } 63 | 64 | b.setRequestInterceptor({ request -> 65 | request.addQueryParam("key", BuildConfig.PLACES_API_KEY) 66 | request.addQueryParam("sensor", "false") 67 | }) 68 | 69 | val restAdapter = b.setEndpoint(BuildConfig.PLACES_API_URL).build() 70 | return restAdapter.create(PlacesApi::class.java) 71 | } 72 | 73 | @Provides @Singleton public fun provideGson(): Gson { 74 | return GsonBuilder().create() 75 | } 76 | 77 | @Provides @Singleton @IOSched public fun provideIoScheduler(): Scheduler { 78 | return Schedulers.io() 79 | } 80 | 81 | @Provides @Singleton @UISched public fun provideUiScheduler(): Scheduler { 82 | return AndroidSchedulers.mainThread() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/BusModule.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app 2 | 3 | import com.squareup.otto.Bus 4 | import com.squareup.otto.ThreadEnforcer 5 | import dagger.Module 6 | import dagger.Provides 7 | import io.dp.weather.app.annotation.PerActivity 8 | import io.dp.weather.app.utils.AsyncBus 9 | 10 | /** 11 | * Created by deepol on 10/09/15. 12 | */ 13 | @Module 14 | public class BusModule { 15 | 16 | @Provides 17 | @PerActivity 18 | public fun provideBus(): Bus { 19 | return AsyncBus(ThreadEnforcer.ANY, "default") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/BusSubcomponent.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app 2 | 3 | import dagger.Subcomponent 4 | import io.dp.weather.app.activity.BaseActivityComponent 5 | import io.dp.weather.app.annotation.PerActivity 6 | import io.dp.weather.app.fragment.WeatherFragment 7 | 8 | /** 9 | * Created by deepol on 16/09/15. 10 | */ 11 | @PerActivity 12 | @Subcomponent(modules = arrayOf(BusModule::class)) 13 | public interface BusSubcomponent : BaseActivityComponent { 14 | 15 | public fun inject(fragment: WeatherFragment) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/Const.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app 2 | 3 | /** 4 | * Created by dp on 08/10/14. 5 | */ 6 | public object Const { 7 | 8 | public val USE_CELCIUS: String = "temperature_switch" 9 | public val USE_KMPH: String = "use_kmph" 10 | public val USE_MMHG: String = "use_mmhg" 11 | 12 | public val FORECAST_FOR_DAYS: Int = 5 13 | public var CONVERT_MMHG: Float = 0.75f 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/SchedulersManager.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app 2 | 3 | import android.support.v4.app.FragmentActivity 4 | import com.trello.rxlifecycle.components.ActivityLifecycleProvider 5 | import io.dp.weather.app.annotation.IOSched 6 | import io.dp.weather.app.annotation.PerActivity 7 | import io.dp.weather.app.annotation.UISched 8 | import rx.Observable 9 | import rx.Scheduler 10 | import javax.inject.Inject 11 | 12 | /** 13 | * Created by deepol on 24/08/15. 14 | */ 15 | @PerActivity 16 | public class SchedulersManager 17 | @Inject 18 | constructor(private val activity: FragmentActivity, 19 | @IOSched private val ioScheduler: Scheduler, 20 | @UISched private val uiScheduler: Scheduler) { 21 | 22 | public fun applySchedulers(): Observable.Transformer 23 | = Observable.Transformer { observable -> 24 | (observable as Observable) 25 | .subscribeOn(ioScheduler) 26 | .observeOn(uiScheduler) 27 | .compose((activity as ActivityLifecycleProvider).bindToLifecycle()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/WeatherApp.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app 2 | 3 | import android.app.Application 4 | 5 | /** 6 | * Created by dp on 07/10/14. 7 | */ 8 | public open class WeatherApp : Application() { 9 | 10 | public var component: AppComponent? = null 11 | private set 12 | 13 | override fun onCreate() { 14 | super.onCreate() 15 | 16 | this.component = createComponent() 17 | } 18 | 19 | public open fun createComponent(): AppComponent { 20 | return DaggerAppComponent.builder().appModule(AppModule(this)).build() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/activity/ActivityComponent.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.activity 2 | 3 | import dagger.Component 4 | import io.dp.weather.app.AppComponent 5 | import io.dp.weather.app.BusModule 6 | import io.dp.weather.app.BusSubcomponent 7 | import io.dp.weather.app.activity.debug.DebugBusModule 8 | import io.dp.weather.app.activity.debug.DebugBusSubcomponent 9 | import io.dp.weather.app.annotation.PerActivity 10 | 11 | @PerActivity 12 | @Component(modules = arrayOf(ActivityModule::class), dependencies = arrayOf(AppComponent::class)) 13 | interface ActivityComponent : BaseActivityComponent { 14 | 15 | fun plusSubComponent(module: BusModule): BusSubcomponent 16 | 17 | fun plusSubComponent(module: DebugBusModule): DebugBusSubcomponent 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/activity/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.activity 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.preference.PreferenceManager 6 | import android.support.v4.app.FragmentActivity 7 | import com.j256.ormlite.android.apptools.OpenHelperManager 8 | import dagger.Module 9 | import dagger.Provides 10 | import io.dp.weather.app.annotation.CachePrefs 11 | import io.dp.weather.app.annotation.ConfigPrefs 12 | import io.dp.weather.app.annotation.PerActivity 13 | import io.dp.weather.app.db.DatabaseHelper 14 | 15 | @Module 16 | class ActivityModule(private val activity: FragmentActivity) { 17 | 18 | @Provides 19 | @PerActivity 20 | fun provideActivity(): FragmentActivity { 21 | return activity 22 | } 23 | 24 | @Provides 25 | @PerActivity 26 | fun provideDatabaseHelper(): DatabaseHelper { 27 | return OpenHelperManager.getHelper(activity, DatabaseHelper::class.java) 28 | } 29 | 30 | @Provides 31 | @ConfigPrefs 32 | @PerActivity 33 | fun provideConfigPrefs(): SharedPreferences { 34 | return PreferenceManager.getDefaultSharedPreferences(activity) 35 | } 36 | 37 | @Provides 38 | @CachePrefs 39 | @PerActivity 40 | fun provideCachePrefs(): SharedPreferences { 41 | return activity.getSharedPreferences("cachePrefs", Context.MODE_PRIVATE) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/activity/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.activity 2 | 3 | import android.os.Bundle 4 | import com.trello.rxlifecycle.components.support.RxAppCompatActivity 5 | import io.dp.weather.app.BusModule 6 | import io.dp.weather.app.WeatherApp 7 | 8 | open class BaseActivity : RxAppCompatActivity(), HasComponent { 9 | private lateinit var component: BaseActivityComponent 10 | 11 | override protected fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | this.component = createComponent() 14 | } 15 | 16 | override fun getComponent(): BaseActivityComponent { 17 | return component 18 | } 19 | 20 | override fun createComponent(): BaseActivityComponent { 21 | val app = getApplication() as WeatherApp 22 | val component = DaggerActivityComponent.builder().appComponent(app.component).activityModule(ActivityModule(this)).build() 23 | 24 | return component.plusSubComponent(BusModule()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/activity/BaseActivityComponent.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.activity 2 | 3 | interface BaseActivityComponent 4 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/activity/HasComponent.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.activity 2 | 3 | interface HasComponent { 4 | 5 | fun createComponent(): T 6 | 7 | fun getComponent(): T 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/activity/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.activity 2 | 3 | import android.os.Bundle 4 | import io.dp.weather.app.R 5 | 6 | class MainActivity : BaseActivity() { 7 | 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | setContentView(R.layout.activity_main) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/activity/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.activity 2 | 3 | import android.annotation.TargetApi 4 | import android.content.Context 5 | import android.content.res.Configuration 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.preference.PreferenceActivity 9 | import android.preference.PreferenceFragment 10 | 11 | import io.dp.weather.app.R 12 | 13 | /** 14 | * A [PreferenceActivity] that presents a set of application settings. On handset devices, 15 | * settings are presented as a single list. On tablets, settings are split by category, with 16 | * category headers shown to the left of the list of settings. 17 | * 18 | * See [ Android Design: Settings](http://developer.android.com/design/patterns/settings.html) 19 | * for design guidelines and the [Settings 20 | * API Guide](http://developer.android.com/guide/topics/ui/settings.html) for more information on developing a Settings UI. 21 | */ 22 | class SettingsActivity : PreferenceActivity() { 23 | 24 | override fun onPostCreate(savedInstanceState: Bundle?) { 25 | super.onPostCreate(savedInstanceState) 26 | 27 | setupSimplePreferencesScreen() 28 | } 29 | 30 | /** 31 | * Shows the simplified settings UI if the device configuration if the device configuration 32 | * dictates that a simplified, single-pane UI should be shown. 33 | */ 34 | @SuppressWarnings("deprecation") 35 | private fun setupSimplePreferencesScreen() { 36 | if (!isSimplePreferences(this)) { 37 | return 38 | } 39 | 40 | // In the simplified UI, fragments are not used at all and we instead 41 | // use the older PreferenceActivity APIs. 42 | 43 | // Add 'general' preferences. 44 | addPreferencesFromResource(R.xml.pref_general) 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | override fun onIsMultiPane(): Boolean { 51 | return isXLargeTablet(this) && !isSimplePreferences(this) 52 | } 53 | 54 | /** 55 | * This fragment shows general preferences only. It is used when the activity is showing a 56 | * two-pane settings UI. 57 | */ 58 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 59 | public class GeneralPreferenceFragment : PreferenceFragment() { 60 | 61 | override fun onCreate(savedInstanceState: Bundle?) { 62 | super.onCreate(savedInstanceState) 63 | addPreferencesFromResource(R.xml.pref_general) 64 | } 65 | } 66 | 67 | companion object { 68 | 69 | /** 70 | * Determines whether to always show the simplified settings UI, where settings are presented in a 71 | * single list. When false, settings are shown as a master/detail two-pane view on tablets. When 72 | * true, a single pane is shown on tablets. 73 | */ 74 | private val ALWAYS_SIMPLE_PREFS = false 75 | 76 | /** 77 | * Helper method to determine if the device has an extra-large screen. For example, 10" tablets 78 | * are extra-large. 79 | */ 80 | private fun isXLargeTablet(context: Context): Boolean { 81 | return (context.resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE 82 | } 83 | 84 | /** 85 | * Determines whether the simplified settings UI should be shown. This is true if this is forced 86 | * via [.ALWAYS_SIMPLE_PREFS], or the device doesn't have newer APIs like [ ], or the device doesn't have an extra-large screen. In these cases, a 87 | * single-pane "simplified" settings UI should be shown. 88 | */ 89 | private fun isSimplePreferences(context: Context): Boolean { 90 | return ALWAYS_SIMPLE_PREFS || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB || !isXLargeTablet(context) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/activity/debug/DebugActivity.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.activity.debug 2 | 3 | import android.os.Bundle 4 | import com.squareup.otto.Bus 5 | import io.dp.weather.app.WeatherApp 6 | import io.dp.weather.app.activity.ActivityModule 7 | import io.dp.weather.app.activity.BaseActivity 8 | import io.dp.weather.app.activity.BaseActivityComponent 9 | import io.dp.weather.app.activity.DaggerActivityComponent 10 | import org.jetbrains.anko.relativeLayout 11 | import org.jetbrains.anko.textView 12 | import javax.inject.Inject 13 | 14 | public class DebugActivity : BaseActivity() { 15 | @Inject lateinit var bus: Bus 16 | 17 | override fun createComponent(): BaseActivityComponent { 18 | val app = getApplication() as WeatherApp 19 | val activityComponent = DaggerActivityComponent.builder() 20 | .appComponent(app.component) 21 | .activityModule(ActivityModule(this)) 22 | .build() 23 | 24 | return activityComponent.plusSubComponent(DebugBusModule()) 25 | } 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | 30 | relativeLayout { 31 | textView("Hello, world!") { 32 | } 33 | } 34 | 35 | (getComponent() as DebugBusSubcomponent).inject(this) 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/activity/debug/DebugBusModule.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.activity.debug 2 | 3 | import com.squareup.otto.Bus 4 | import com.squareup.otto.ThreadEnforcer 5 | import dagger.Module 6 | import dagger.Provides 7 | import io.dp.weather.app.annotation.PerActivity 8 | import io.dp.weather.app.utils.AsyncBus 9 | 10 | /** 11 | * Created by deepol on 10/09/15. 12 | */ 13 | @Module 14 | public class DebugBusModule { 15 | 16 | @Provides 17 | @PerActivity 18 | // in Kotlin we need to have different method name for overriden dependency 19 | // the reason - dagger2 + kapt won't generate BusModule_Factory code 20 | public fun provideDebugBus(): Bus { 21 | return AsyncBus(ThreadEnforcer.ANY, "debug") 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/activity/debug/DebugBusSubcomponent.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.activity.debug 2 | 3 | import dagger.Subcomponent 4 | import io.dp.weather.app.BusSubcomponent 5 | import io.dp.weather.app.annotation.PerActivity 6 | 7 | /** 8 | * Created by deepol on 16/09/15. 9 | */ 10 | @PerActivity 11 | @Subcomponent(modules = arrayOf(DebugBusModule::class)) 12 | public interface DebugBusSubcomponent : BusSubcomponent { 13 | 14 | public fun inject(debugActivity: DebugActivity) 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/adapter/CursorRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.adapter 2 | 3 | 4 | import android.database.Cursor 5 | import android.database.DataSetObserver 6 | import android.support.v7.widget.RecyclerView 7 | 8 | abstract class CursorRecyclerViewAdapter(cursor: Cursor?) : RecyclerView.Adapter() { 9 | 10 | private var cursor: Cursor? 11 | private var dataValid: Boolean 12 | private var rowIdColumn: Int 13 | private val dataSetObserver: DataSetObserver? 14 | 15 | init { 16 | this.cursor = cursor 17 | dataValid = cursor != null 18 | rowIdColumn = if (dataValid) this.cursor!!.getColumnIndex("_id") else -1 19 | dataSetObserver = NotifyingDataSetObserver() 20 | this.cursor?.registerDataSetObserver(dataSetObserver) 21 | } 22 | 23 | override fun getItemCount(): Int { 24 | return cursor?.count ?: 0 25 | } 26 | 27 | override fun getItemId(position: Int): Long { 28 | if (dataValid && cursor?.moveToPosition(position) ?: false) { 29 | return cursor?.getLong(rowIdColumn) ?: 0 30 | } 31 | return 0 32 | } 33 | 34 | fun getItem(position: Int): Any? { 35 | when { 36 | dataValid -> { 37 | cursor?.moveToPosition(position) 38 | return cursor 39 | } 40 | else -> return null 41 | } 42 | } 43 | 44 | override fun setHasStableIds(hasStableIds: Boolean) { 45 | super.setHasStableIds(true) 46 | } 47 | 48 | abstract fun onBindViewHolder(viewHolder: VH, cursor: Cursor?) 49 | 50 | override fun onBindViewHolder(viewHolder: VH, position: Int) { 51 | if (!dataValid) { 52 | throw IllegalStateException("this should only be called when the cursor is valid") 53 | } 54 | if (!cursor!!.moveToPosition(position)) { 55 | throw IllegalStateException("couldn't move cursor to position " + position) 56 | } 57 | onBindViewHolder(viewHolder, cursor) 58 | } 59 | 60 | /** 61 | * Change the underlying cursor to a new cursor. If there is an existing cursor it will be 62 | * closed. 63 | */ 64 | open fun changeCursor(cursor: Cursor?) { 65 | if (cursor != null) { 66 | val old = swapCursor(cursor) 67 | old?.close() 68 | } 69 | } 70 | 71 | /** 72 | * Swap in a new Cursor, returning the old Cursor. Unlike 73 | * [.changeCursor], the returned old Cursor is *not* 74 | * closed. 75 | */ 76 | fun swapCursor(newCursor: Cursor): Cursor? { 77 | if (newCursor === cursor) { 78 | return null 79 | } 80 | val oldCursor = cursor 81 | if (oldCursor != null && dataSetObserver != null) { 82 | oldCursor.unregisterDataSetObserver(dataSetObserver) 83 | } 84 | cursor = newCursor 85 | if (cursor != null) { 86 | if (dataSetObserver != null) { 87 | cursor!!.registerDataSetObserver(dataSetObserver) 88 | } 89 | rowIdColumn = newCursor.getColumnIndexOrThrow("_id") 90 | dataValid = true 91 | notifyDataSetChanged() 92 | } else { 93 | rowIdColumn = -1 94 | dataValid = false 95 | notifyDataSetChanged() 96 | //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter 97 | } 98 | return oldCursor 99 | } 100 | 101 | private inner class NotifyingDataSetObserver : DataSetObserver() { 102 | override fun onChanged() { 103 | super.onChanged() 104 | dataValid = true 105 | notifyDataSetChanged() 106 | } 107 | 108 | override fun onInvalidated() { 109 | super.onInvalidated() 110 | dataValid = false 111 | notifyDataSetChanged() 112 | //There is no notifyDataSetInvalidated() method in RecyclerView.Adapter 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/adapter/OrmliteCursorRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.adapter 2 | 3 | import android.database.Cursor 4 | import android.support.v7.widget.RecyclerView 5 | import com.j256.ormlite.android.AndroidDatabaseResults 6 | import com.j256.ormlite.stmt.PreparedQuery 7 | import java.sql.SQLException 8 | 9 | abstract class OrmliteCursorRecyclerViewAdapter() : CursorRecyclerViewAdapter(null) { 10 | var preparedQuery: PreparedQuery? = null 11 | 12 | abstract fun onBindViewHolder(holder: VH, t: T) 13 | 14 | override fun onBindViewHolder(viewHolder: VH, cursor: Cursor?) { 15 | try { 16 | onBindViewHolder(viewHolder, this.cursorToObject(cursor!!)) 17 | } catch (e: SQLException) { 18 | e.printStackTrace() 19 | } 20 | } 21 | 22 | override fun changeCursor(cursor: Cursor?) { 23 | this.preparedQuery = preparedQuery 24 | super.changeCursor(cursor) 25 | } 26 | 27 | fun changeCursor(cursor: Cursor, preparedQuery: PreparedQuery) { 28 | this.preparedQuery = preparedQuery 29 | super.changeCursor(cursor) 30 | } 31 | 32 | fun getTypedItem(position: Int): T { 33 | try { 34 | return this.cursorToObject(getItem(position) as Cursor) 35 | } catch (var3: SQLException) { 36 | throw RuntimeException(var3) 37 | } 38 | } 39 | 40 | @Throws(SQLException::class) 41 | protected fun cursorToObject(cursor: Cursor): T { 42 | return this.preparedQuery!!.mapRow(AndroidDatabaseResults(cursor, null)) 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/adapter/PlacesAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.adapter 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.support.v4.app.FragmentActivity 6 | import android.support.v7.widget.RecyclerView 7 | import android.text.format.DateUtils 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.PopupMenu 11 | import com.google.gson.Gson 12 | import com.squareup.otto.Bus 13 | import com.squareup.picasso.Picasso 14 | import io.dp.weather.app.Const 15 | import io.dp.weather.app.R 16 | import io.dp.weather.app.SchedulersManager 17 | import io.dp.weather.app.annotation.CachePrefs 18 | import io.dp.weather.app.annotation.PerActivity 19 | import io.dp.weather.app.db.table.Place 20 | import io.dp.weather.app.event.DeletePlaceEvent 21 | import io.dp.weather.app.net.WeatherApi 22 | import io.dp.weather.app.net.dto.Forecast 23 | import io.dp.weather.app.net.dto.Weather 24 | import io.dp.weather.app.utils.MetricsController 25 | import io.dp.weather.app.utils.WhiteBorderCircleTransformation 26 | import io.dp.weather.app.utils.myEdit 27 | import kotlinx.android.synthetic.item_city_weather.view.* 28 | import kotlinx.android.synthetic.item_place_content.view.* 29 | import rx.lang.kotlin.subscribeWith 30 | import timber.log.Timber 31 | import javax.inject.Inject 32 | 33 | @PerActivity 34 | class PlacesAdapter 35 | @Inject constructor(val activity: FragmentActivity, 36 | val gson: Gson, 37 | val api: WeatherApi, 38 | val bus: Bus) : OrmliteCursorRecyclerViewAdapter() { 39 | 40 | private val cache = android.support.v4.util.LruCache(16) 41 | 42 | private lateinit var schedulersManager: SchedulersManager 43 | private lateinit var prefs: SharedPreferences 44 | private lateinit var metrics: MetricsController 45 | 46 | private val transformation = WhiteBorderCircleTransformation() 47 | 48 | @Inject 49 | fun setMetricsController(metrics: MetricsController) { 50 | this.metrics = metrics 51 | } 52 | 53 | @Inject 54 | fun setSharedPreferences(@CachePrefs prefs: SharedPreferences) { 55 | this.prefs = prefs 56 | } 57 | 58 | @Inject 59 | fun setSchedulersManager(schedulersManager: SchedulersManager) { 60 | this.schedulersManager = schedulersManager 61 | } 62 | 63 | public fun clear() { 64 | this.cache.evictAll() 65 | this.prefs.edit().clear().apply() 66 | } 67 | 68 | fun SharedPreferences.isNeedToUpdateWeather(hash: String): Boolean { 69 | val lastRequestTime = prefs.getLong(hash + "_time", -1) 70 | return lastRequestTime == -1L 71 | || (lastRequestTime > 0 72 | && (System.currentTimeMillis() - lastRequestTime) > DateUtils.DAY_IN_MILLIS) 73 | } 74 | 75 | override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder? { 76 | return Holder(activity.layoutInflater.inflate(R.layout.item_city_weather, parent, false)) 77 | } 78 | 79 | fun getForecast(place: Place): Forecast? { 80 | val hash = "${place.hashCode()}" 81 | var forecast = cache.get(place.id) 82 | if (forecast == null && place.id != null) { 83 | // forecast exists - load it from cache 84 | val rawForecast = prefs.getString(hash, null) 85 | forecast = gson.fromJson(rawForecast, Forecast::class.java) 86 | forecast?.let { 87 | cache.put(place.id, forecast) 88 | } 89 | } 90 | 91 | return forecast 92 | } 93 | 94 | override fun onBindViewHolder(holder: Holder, place: Place) { 95 | val hash = "${place.hashCode()}" 96 | 97 | val isNeedToUpdate = prefs.isNeedToUpdateWeather(hash) 98 | 99 | with (holder) { 100 | with (itemView) { 101 | city_name.text = place.name 102 | temperature.text = "" 103 | 104 | menu.tag = place.id 105 | menu.setOnClickListener(popupOnClickListener) 106 | 107 | when { 108 | metrics.useCelsius -> degrees_type.setText(R.string.celcius) 109 | else -> degrees_type.setText(R.string.fahrenheit) 110 | } 111 | 112 | progress.visibility = if (isNeedToUpdate) View.VISIBLE else View.GONE 113 | content.visibility = if (isNeedToUpdate) View.GONE else View.VISIBLE 114 | } 115 | } 116 | 117 | if (isNeedToUpdate) { 118 | api.getForecast("${place.lat},${place.lon}", Const.FORECAST_FOR_DAYS) 119 | .compose(schedulersManager.applySchedulers()) 120 | .subscribeWith { 121 | onNext { 122 | prefs.myEdit { 123 | arrayOf(hash + "_time" to System.currentTimeMillis(), 124 | hash to gson.toJson(it)) 125 | } 126 | notifyDataSetChanged() 127 | } 128 | 129 | onError { 130 | Timber.e(it, "Got throwable") 131 | } 132 | } 133 | } else { 134 | val forecast = getForecast(place) ?: Forecast() 135 | holder.fillViewWithForecast(activity, forecast, metrics, transformation) 136 | } 137 | } 138 | 139 | val popupOnClickListener: View.OnClickListener = object : View.OnClickListener { 140 | override fun onClick(v: View) { 141 | val id = v.tag as Long 142 | 143 | val popupMenu = PopupMenu(activity, v) 144 | 145 | with (popupMenu) { 146 | inflate(R.menu.item_place) 147 | 148 | setOnMenuItemClickListener { 149 | bus.post(DeletePlaceEvent(id)) 150 | true 151 | } 152 | 153 | show() 154 | } 155 | 156 | } 157 | } 158 | 159 | class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) { 160 | 161 | fun fillViewWithForecast(context: Context, 162 | forecast: Forecast, 163 | metrics: MetricsController, 164 | transformation: WhiteBorderCircleTransformation) { 165 | with (itemView) { 166 | val conditions = forecast.data?.currentCondition 167 | 168 | conditions?.isNotEmpty().let { 169 | val condition = conditions.get(0) 170 | 171 | humidity.text = "${condition.humidity} %" 172 | 173 | try { 174 | val pressure_val = Integer.valueOf(condition.pressure)!! 175 | 176 | pressure.text = when { 177 | metrics.useMmhg -> context.getString(R.string.fmt_pressure_mmhg, (pressure_val * Const.CONVERT_MMHG).toInt()) 178 | else -> context.getString(R.string.fmt_pressure_kpa, pressure_val) 179 | } 180 | } catch (e: NumberFormatException) { 181 | pressure.setText(R.string.undef) 182 | } 183 | 184 | val metric = when { 185 | metrics.useKmph -> context.getString(R.string.fmt_windspeed_kmph, condition.windspeedKmph ?: "") 186 | else -> context.getString(R.string.fmt_windspeed_mph, condition.windspeedMiles ?: "") 187 | } 188 | 189 | wind.text = "${condition.winddir16Point}, $metric" 190 | 191 | val descList = condition.weatherDesc 192 | if (descList?.isNotEmpty() ?: false) { 193 | val description = descList?.get(0)?.value ?: "" 194 | weather_description.text = description 195 | } 196 | 197 | when { 198 | metrics.useCelsius -> temperature.text = condition.tempC ?: "" 199 | else -> temperature.text = condition.tempF ?: "" 200 | } 201 | 202 | val urls = conditions.get(0).weatherIconUrl 203 | 204 | if (urls?.isNotEmpty() ?: false) { 205 | val url = urls?.get(0)?.value ?: "" 206 | Picasso.with(context) 207 | .load(url) 208 | .transform(transformation) 209 | .into(weather_state) 210 | } 211 | } 212 | 213 | val weather5days = forecast.data?.weather ?: listOf() 214 | weather_for_week.setWeatherForWeek(weather5days, metrics.useCelsius, transformation) 215 | } 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/adapter/PlacesAutoCompleteAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.adapter 2 | 3 | import android.support.v4.app.FragmentActivity 4 | import android.widget.ArrayAdapter 5 | import android.widget.Filter 6 | import android.widget.Filterable 7 | import com.google.gson.JsonObject 8 | import io.dp.weather.app.R 9 | import io.dp.weather.app.net.PlacesApi 10 | import java.util.* 11 | import javax.inject.Inject 12 | 13 | class PlacesAutoCompleteAdapter 14 | @Inject 15 | constructor(activity: FragmentActivity, private val placesApi: PlacesApi) : 16 | ArrayAdapter(activity, R.layout.item_search_list), Filterable { 17 | 18 | private var resultList: ArrayList? = null 19 | 20 | override fun getCount(): Int = resultList?.size ?: 0 21 | 22 | override fun getItem(index: Int): String = resultList?.get(index) ?: "" 23 | 24 | override fun getFilter(): Filter { 25 | return object : Filter() { 26 | override fun performFiltering(constraint: CharSequence?): Filter.FilterResults { 27 | val filterResults = Filter.FilterResults() 28 | if (constraint != null) { 29 | val jsonResults = placesApi.getAutocomplete(constraint.toString()) 30 | 31 | // Create a JSON object hierarchy from the results 32 | val predsJsonArray = jsonResults.getAsJsonArray("predictions") 33 | 34 | // Extract the Place descriptions from the results 35 | resultList = ArrayList(predsJsonArray.size()) 36 | for (i in 0..predsJsonArray.size() - 1) { 37 | val o = predsJsonArray[i] as JsonObject 38 | resultList?.add(o.get("description").asString) 39 | } 40 | 41 | // Assign the data to the FilterResults 42 | filterResults.values = resultList 43 | filterResults.count = resultList?.size ?: 0 44 | } 45 | return filterResults 46 | } 47 | 48 | override fun publishResults(constraint: CharSequence?, 49 | results: Filter.FilterResults?) = when { 50 | results?.count ?: -1 > 0 -> notifyDataSetChanged() 51 | else -> notifyDataSetInvalidated() 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/annotation/CachePrefs.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.annotation 2 | 3 | import javax.inject.Qualifier 4 | 5 | import kotlin.annotation.AnnotationRetention.RUNTIME 6 | 7 | /** 8 | * Created by dp on 11/10/14. 9 | */ 10 | @Qualifier 11 | @MustBeDocumented 12 | @Retention(RUNTIME) 13 | annotation public class CachePrefs 14 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/annotation/ConfigPrefs.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.annotation 2 | 3 | import javax.inject.Qualifier 4 | 5 | import kotlin.annotation.AnnotationRetention.RUNTIME 6 | 7 | /** 8 | * Created by dp on 11/10/14. 9 | */ 10 | @Qualifier 11 | @MustBeDocumented 12 | @Retention(RUNTIME) 13 | annotation public class ConfigPrefs 14 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/annotation/IOSched.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.annotation 2 | 3 | import javax.inject.Qualifier 4 | 5 | import kotlin.annotation.AnnotationRetention.RUNTIME 6 | 7 | /** 8 | * Created by deepol on 11/09/15. 9 | */ 10 | @Qualifier 11 | @MustBeDocumented 12 | @Retention(RUNTIME) 13 | annotation public class IOSched 14 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/annotation/PerActivity.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.annotation 2 | 3 | import javax.inject.Scope 4 | 5 | import kotlin.annotation.AnnotationRetention.RUNTIME 6 | 7 | /** 8 | * A scoping annotation to permit objects whose lifetime should 9 | * conform to the life of the activity to be memorized in the 10 | * correct component. 11 | */ 12 | @Scope @Retention(RUNTIME) annotation public class PerActivity 13 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/annotation/PerFragment.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.annotation 2 | 3 | import javax.inject.Scope 4 | 5 | import kotlin.annotation.AnnotationRetention.RUNTIME 6 | 7 | /** 8 | * A scoping annotation to permit objects whose lifetime should 9 | * conform to the life of the activity to be memorized in the 10 | * correct component. 11 | */ 12 | @Scope @Retention(RUNTIME) annotation public class PerFragment 13 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/annotation/UISched.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.annotation 2 | 3 | import javax.inject.Qualifier 4 | 5 | import kotlin.annotation.AnnotationRetention.RUNTIME 6 | 7 | /** 8 | * Created by deepol on 11/09/15. 9 | */ 10 | @Qualifier 11 | @MustBeDocumented 12 | @Retention(RUNTIME) 13 | annotation public class UISched 14 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/db/DatabaseHelper.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.db 2 | 3 | import android.content.Context 4 | import android.database.sqlite.SQLiteDatabase 5 | import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper 6 | import com.j256.ormlite.dao.Dao 7 | import com.j256.ormlite.support.ConnectionSource 8 | import com.j256.ormlite.table.TableUtils 9 | import io.dp.weather.app.BuildConfig 10 | import io.dp.weather.app.R 11 | import io.dp.weather.app.db.table.Place 12 | import timber.log.Timber 13 | import java.sql.SQLException 14 | 15 | /** 16 | * Created by dp on 09/10/14. 17 | */ 18 | public class DatabaseHelper : OrmLiteSqliteOpenHelper { 19 | 20 | var context: Context 21 | 22 | @Volatile private var placeDao: Dao? = null 23 | 24 | var predefinedCities = arrayOf( 25 | Place("Dublin", 53.34410, -6.2674), 26 | Place("London", 51.51121, -0.1198), 27 | Place("New York", 40.71278, -74.00594), 28 | Place("Barcelona", 41.3850, 2.1734)) 29 | 30 | public constructor(context: Context, databaseName: String, factory: SQLiteDatabase.CursorFactory, 31 | databaseVersion: Int) : super(context, databaseName, factory, databaseVersion, R.raw.ormlite_config) { 32 | this.context = context 33 | } 34 | 35 | public constructor(context: Context) : super(context, DATABASE_NAME, null, DATABASE_VERSION, R.raw.ormlite_config) { 36 | this.context = context 37 | } 38 | 39 | override fun onCreate(database: SQLiteDatabase, connectionSource: ConnectionSource) { 40 | Timber.v("! onCreateDatabase") 41 | 42 | try { 43 | TableUtils.createTableIfNotExists(connectionSource, Place::class.java) 44 | val cityDao = getPlaceDao() 45 | for (place in predefinedCities) { 46 | cityDao?.createIfNotExists(place) 47 | } 48 | } catch (e: SQLException) { 49 | e.printStackTrace() 50 | } 51 | 52 | } 53 | 54 | override fun onUpgrade(database: SQLiteDatabase, connectionSource: ConnectionSource, oldVersion: Int, 55 | newVersion: Int) { 56 | Timber.v("! onUpgradeDatabase") 57 | } 58 | 59 | @Throws(SQLException::class) 60 | public fun getPlaceDao(): Dao? { 61 | var resultDao = placeDao 62 | 63 | if (resultDao == null) { 64 | synchronized (this) { 65 | resultDao = placeDao 66 | 67 | if (resultDao == null) { 68 | resultDao = getDao(Place::class.java) 69 | placeDao = resultDao 70 | } 71 | } 72 | } 73 | return resultDao 74 | } 75 | 76 | override fun close() { 77 | super.close() 78 | 79 | placeDao = null 80 | } 81 | 82 | companion object { 83 | 84 | private val DATABASE_NAME = "weather.db" 85 | private val DATABASE_VERSION = BuildConfig.DATABASE_VERSION 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/db/NoIdCursorWrapper.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.db 2 | 3 | import android.database.Cursor 4 | import android.database.CursorWrapper 5 | import android.provider.BaseColumns 6 | 7 | /** 8 | * Created by dp on 09/10/14. 9 | */ 10 | public class NoIdCursorWrapper : CursorWrapper { 11 | 12 | private var idColumnIndex: Int = 0 13 | 14 | /** 15 | * Create a NoIdCursorWrapper using the alias column index. 16 | 17 | * @param c the cursor to wrap 18 | * * 19 | * @param idColumnIndex the column index to use as the _id column alias 20 | */ 21 | public constructor(c: Cursor, idColumnIndex: Int) : super(c) { 22 | this.idColumnIndex = idColumnIndex 23 | } 24 | 25 | /** 26 | * Create a NoIdCursorWrapper using the alias column name. 27 | 28 | * @param c the cursor to wrap 29 | * * 30 | * @param idColumnName the column name to use as the _id column alias 31 | */ 32 | public constructor(c: Cursor, idColumnName: String) : super(c) { 33 | idColumnIndex = c.getColumnIndex(idColumnName) 34 | } 35 | 36 | override fun getColumnIndex(columnName: String): Int { 37 | var index = super.getColumnIndex(columnName) 38 | if (index < 0 && BaseColumns._ID == columnName) { 39 | index = idColumnIndex 40 | } 41 | return index 42 | } 43 | 44 | @Throws(IllegalArgumentException::class) 45 | override fun getColumnIndexOrThrow(columnName: String): Int { 46 | val index = getColumnIndex(columnName) 47 | if (index >= 0) { 48 | return index 49 | } 50 | // let the AbstractCursor generate the exception 51 | return super.getColumnIndexOrThrow(columnName) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/db/OrmliteCursorAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.db 2 | 3 | import android.content.Context 4 | import android.database.Cursor 5 | import android.support.v4.widget.CursorAdapter 6 | import android.view.View 7 | 8 | import com.j256.ormlite.android.AndroidDatabaseResults 9 | import com.j256.ormlite.stmt.PreparedQuery 10 | 11 | import java.sql.SQLException 12 | 13 | /** 14 | * Created by dp on 09/10/14. 15 | */ 16 | public abstract class OrmliteCursorAdapter(context: Context, c: Cursor?, public var query: PreparedQuery?) : CursorAdapter(context, c, false) { 17 | 18 | override public fun bindView(itemView: View, context: Context, cursor: Cursor) { 19 | try { 20 | var item = query?.mapRow(AndroidDatabaseResults(cursor, null)) 21 | bindView(itemView, context, item) 22 | } catch (e: SQLException) { 23 | e.printStackTrace() 24 | } 25 | 26 | } 27 | 28 | override public fun getItem(position: Int): T? { 29 | val c = super.getItem(position) as Cursor 30 | try { 31 | return query?.mapRow(AndroidDatabaseResults(c, null)) 32 | } catch (e: SQLException) { 33 | e.printStackTrace() 34 | } 35 | 36 | return null 37 | } 38 | 39 | public abstract fun bindView(itemView: View, context: Context, item: T?) 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/db/OrmliteCursorLoader.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.db 2 | 3 | import android.content.Context 4 | import android.database.Cursor 5 | import android.support.v4.content.AsyncTaskLoader 6 | import android.support.v4.content.Loader 7 | import com.j256.ormlite.android.AndroidDatabaseResults 8 | import com.j256.ormlite.dao.BaseDaoImpl 9 | import com.j256.ormlite.dao.Dao 10 | import com.j256.ormlite.stmt.PreparedQuery 11 | import java.io.FileDescriptor 12 | import java.io.PrintWriter 13 | import java.sql.SQLException 14 | 15 | /** 16 | * Created by dp on 09/10/14. 17 | */ 18 | public class OrmliteCursorLoader(context: Context, public val dao: Dao?, 19 | public val query: PreparedQuery?) : AsyncTaskLoader(context) { 20 | 21 | val observer: Loader.ForceLoadContentObserver = Loader(context).ForceLoadContentObserver() 22 | 23 | private var cursor: Cursor? = null 24 | 25 | /* Runs on a worker thread */ 26 | 27 | override fun loadInBackground(): Cursor? { 28 | var cursor: Cursor? = null 29 | try { 30 | cursor = getCursor(query) 31 | } catch (e: SQLException) { 32 | e.printStackTrace() 33 | } 34 | 35 | if (cursor != null) { 36 | // Ensure the cursor window is filled 37 | cursor.count 38 | registerContentObserver(cursor) 39 | } 40 | return cursor 41 | } 42 | 43 | @Throws(SQLException::class) 44 | public fun getCursor(query: PreparedQuery?): Cursor { 45 | val baseDao = dao as BaseDaoImpl 46 | val iterator = dao.iterator(query) 47 | val results = iterator.getRawResults() as AndroidDatabaseResults 48 | val idColumnName = baseDao.getTableInfo().getIdField().getColumnName() 49 | return NoIdCursorWrapper(results.getRawCursor(), idColumnName) 50 | } 51 | 52 | /** 53 | * Registers an observer to get notifications from the content provider when the cursor needs to 54 | * be refreshed. 55 | */ 56 | fun registerContentObserver(cursor: Cursor) { 57 | cursor.registerContentObserver(this.observer) 58 | } 59 | 60 | /* Runs on the UI thread */ 61 | override public fun deliverResult(cursor: Cursor?) { 62 | if (isReset()) { 63 | // An async query came in while the loader is stopped 64 | cursor?.close() 65 | return 66 | } 67 | val oldCursor = this.cursor 68 | this.cursor = cursor 69 | 70 | if (isStarted()) { 71 | super.deliverResult(cursor) 72 | } 73 | 74 | if (oldCursor != null && oldCursor !== cursor && !oldCursor.isClosed) { 75 | oldCursor.close() 76 | } 77 | } 78 | 79 | override fun onStartLoading() { 80 | if (cursor != null) { 81 | deliverResult(cursor) 82 | } 83 | if (takeContentChanged() || cursor == null) { 84 | forceLoad() 85 | } 86 | } 87 | 88 | /** 89 | * Must be called from the UI thread 90 | */ 91 | override fun onStopLoading() { 92 | // Attempt to cancel the current load task if possible. 93 | cancelLoad() 94 | } 95 | 96 | override fun onCanceled(cursor: Cursor?) { 97 | if (cursor != null && !cursor.isClosed) { 98 | cursor.close() 99 | } 100 | } 101 | 102 | override fun onReset() { 103 | super.onReset() 104 | 105 | // Ensure the loader is stopped 106 | onStopLoading() 107 | 108 | if (cursor != null && !cursor!!.isClosed) { 109 | cursor!!.close() 110 | } 111 | cursor = null 112 | } 113 | 114 | override fun dump(prefix: String, fd: FileDescriptor, writer: PrintWriter, args: Array) { 115 | super.dump(prefix, fd, writer, args) 116 | writer.print(prefix) 117 | writer.print("cursor=") 118 | writer.println(cursor) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/db/OrmliteListLoader.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.db 2 | 3 | import android.content.Context 4 | import android.support.v4.content.AsyncTaskLoader 5 | 6 | import com.j256.ormlite.dao.Dao 7 | import com.j256.ormlite.stmt.PreparedQuery 8 | 9 | import java.sql.SQLException 10 | import java.util.* 11 | 12 | /** 13 | * Created by dp on 09/10/14. 14 | */ 15 | public class OrmliteListLoader(context: Context, dao: Dao, query: PreparedQuery) : AsyncTaskLoader>(context) { 16 | 17 | private lateinit var dao: Dao 18 | private lateinit var query: PreparedQuery 19 | private var data: List? = null 20 | 21 | init { 22 | this.dao = dao 23 | this.query = query 24 | } 25 | 26 | override fun loadInBackground(): List { 27 | val result: ArrayList = ArrayList() 28 | 29 | try { 30 | result.addAll(dao.query(query)) 31 | 32 | } catch (e: SQLException) { 33 | } 34 | 35 | return result 36 | } 37 | 38 | override fun deliverResult(datas: List?) { 39 | if (isReset()) { 40 | // An async query came in while the loader is stopped. We 41 | // don't need the result. 42 | if (datas != null) { 43 | onReleaseResources(datas) 44 | } 45 | } 46 | 47 | val oldDatas = data 48 | data = datas 49 | 50 | if (isStarted()) { 51 | // If the Loader is currently started, we can immediately 52 | // deliver its results. 53 | super.deliverResult(datas) 54 | } 55 | 56 | if (oldDatas != null && !oldDatas.isEmpty()) { 57 | onReleaseResources(oldDatas) 58 | } 59 | } 60 | 61 | /** 62 | * Handles a request to start the Loader. 63 | */ 64 | override fun onStartLoading() { 65 | if (data != null) { 66 | // If we currently have a result available, deliver it 67 | // immediately. 68 | deliverResult(data) 69 | } else { 70 | // If the data has changed since the last time it was loaded 71 | // or is not currently available, start a load. 72 | forceLoad() 73 | } 74 | } 75 | 76 | /** 77 | * Handles a request to stop the Loader. 78 | */ 79 | override fun onStopLoading() { 80 | // Attempt to cancel the current load task if possible. 81 | cancelLoad() 82 | } 83 | 84 | /** 85 | * Handles a request to cancel a load. 86 | */ 87 | override fun onCanceled(datas: List) { 88 | super.onCanceled(datas) 89 | 90 | // At this point we can release the resources associated with 'apps' 91 | // if needed. 92 | onReleaseResources(datas) 93 | } 94 | 95 | /** 96 | * Handles a request to completely reset the Loader. 97 | */ 98 | override fun onReset() { 99 | super.onReset() 100 | 101 | // Ensure the loader is stopped 102 | onStopLoading() 103 | 104 | // At this point we can release the resources associated with 'apps' 105 | // if needed. 106 | if (data != null) { 107 | onReleaseResources(data) 108 | data = null 109 | } 110 | } 111 | 112 | /** 113 | * Helper function to take care of releasing resources associated with an actively loaded data 114 | * set. 115 | */ 116 | protected fun onReleaseResources(datas: List?) { 117 | // For a simple List<> there is nothing to do. For something 118 | // like a Cursor, we would close it here. 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/db/Queries.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.db 2 | 3 | import com.j256.ormlite.stmt.PreparedQuery 4 | import io.dp.weather.app.db.table.Place 5 | import java.sql.SQLException 6 | 7 | /** 8 | * Created by dp on 09/10/14. 9 | */ 10 | public object Queries { 11 | 12 | public fun prepareCityQuery(dbHelper: DatabaseHelper?): PreparedQuery? { 13 | try { 14 | val qb = dbHelper!!.getPlaceDao()!!.queryBuilder() 15 | return qb.prepare() 16 | } catch (e: SQLException) { 17 | e.printStackTrace() 18 | } 19 | 20 | return null 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/db/Tables.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.db 2 | 3 | /** 4 | * Created by deepol on 10/11/15. 5 | */ 6 | object PlaceTable { 7 | val NAME = "Place" 8 | val ID = "_id" 9 | val CITY = "city" 10 | val LAT = "lat" 11 | val LON = "LON" 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/db/table/Place.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.db.table 2 | 3 | import com.j256.ormlite.field.DatabaseField 4 | import com.j256.ormlite.table.DatabaseTable 5 | 6 | @DatabaseTable(tableName = "places") 7 | data class Place(@DatabaseField(generatedId = true, dataType = com.j256.ormlite.field.DataType.LONG, columnName = Place.ID) 8 | public var id: Long? = 0, 9 | 10 | @DatabaseField(dataType = com.j256.ormlite.field.DataType.STRING, columnName = Place.NAME) 11 | public var name: String? = null, 12 | 13 | @DatabaseField(dataType = com.j256.ormlite.field.DataType.DOUBLE_OBJ, columnName = Place.LAT) 14 | public var lat: Double? = null, 15 | 16 | @DatabaseField(dataType = com.j256.ormlite.field.DataType.DOUBLE_OBJ, columnName = Place.LON) 17 | public var lon: Double? = null) { 18 | 19 | public constructor(name: String, lat: Double, lon: Double) : this() { 20 | this.name = name 21 | this.lat = lat 22 | this.lon = lon 23 | } 24 | 25 | companion object { 26 | const public val ID = "id" 27 | const public val NAME = "name" 28 | const public val LAT = "lat" 29 | const public val LON = "lon" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/event/AddPlaceEvent.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.event 2 | 3 | /** 4 | * Created by dp on 10/10/14. 5 | */ 6 | public class AddPlaceEvent(public val lookupPlace: String) 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/event/DeletePlaceEvent.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.event 2 | 3 | /** 4 | * Created by dp on 09/10/14. 5 | */ 6 | public class DeletePlaceEvent(public val id: Long?) 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/event/UpdateListEvent.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.event 2 | 3 | /** 4 | * Created by dp on 10/10/14. 5 | */ 6 | public class UpdateListEvent 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/fragment/BaseFragment.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.fragment 2 | 3 | import android.os.Bundle 4 | import com.trello.rxlifecycle.components.support.RxFragment 5 | import io.dp.weather.app.activity.BaseActivityComponent 6 | import io.dp.weather.app.activity.HasComponent 7 | 8 | /** 9 | * Created by dp on 08/10/14. 10 | */ 11 | public abstract class BaseFragment : RxFragment() { 12 | 13 | public lateinit var component: BaseActivityComponent 14 | 15 | override fun onActivityCreated(savedInstanceState: Bundle?) { 16 | super.onActivityCreated(savedInstanceState) 17 | 18 | val activity = getActivity() as HasComponent 19 | this.component = activity.getComponent() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/fragment/WeatherFragment.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.fragment 2 | 3 | import android.content.Intent 4 | import android.database.Cursor 5 | import android.location.Geocoder 6 | import android.os.Bundle 7 | import android.support.v4.app.LoaderManager 8 | import android.support.v4.content.Loader 9 | import android.support.v4.view.MenuItemCompat 10 | import android.support.v4.widget.SwipeRefreshLayout 11 | import android.support.v7.widget.StaggeredGridLayoutManager 12 | import android.view.* 13 | import android.widget.AdapterView 14 | import com.squareup.otto.Bus 15 | import com.squareup.otto.Subscribe 16 | import io.dp.weather.app.BusSubcomponent 17 | import io.dp.weather.app.R 18 | import io.dp.weather.app.SchedulersManager 19 | import io.dp.weather.app.activity.SettingsActivity 20 | import io.dp.weather.app.activity.debug.DebugActivity 21 | import io.dp.weather.app.adapter.PlacesAdapter 22 | import io.dp.weather.app.adapter.PlacesAutoCompleteAdapter 23 | import io.dp.weather.app.db.DatabaseHelper 24 | import io.dp.weather.app.db.OrmliteCursorLoader 25 | import io.dp.weather.app.db.Queries 26 | import io.dp.weather.app.db.table.Place 27 | import io.dp.weather.app.event.AddPlaceEvent 28 | import io.dp.weather.app.event.DeletePlaceEvent 29 | import io.dp.weather.app.event.UpdateListEvent 30 | import io.dp.weather.app.utils.Observables 31 | import io.dp.weather.app.widget.ArrayAdapterSearchView 32 | import org.jetbrains.anko.support.v4.longToast 33 | import rx.lang.kotlin.subscribeWith 34 | import java.sql.SQLException 35 | import javax.inject.Inject 36 | 37 | class WeatherFragment : BaseFragment(), LoaderManager.LoaderCallbacks, SwipeRefreshLayout.OnRefreshListener { 38 | 39 | @Inject lateinit var geoCoder: Geocoder 40 | @Inject lateinit var adapter: PlacesAdapter 41 | @Inject lateinit var dbHelper: DatabaseHelper 42 | @Inject lateinit var bus: Bus 43 | @Inject lateinit var placesAutoCompleteAdapter: PlacesAutoCompleteAdapter 44 | @Inject lateinit var schedulersManager: SchedulersManager 45 | 46 | override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, 47 | savedInstanceState: Bundle?): View? { 48 | return inflater!!.inflate(R.layout.fragment_weather, container, false) 49 | } 50 | 51 | override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { 52 | super.onViewCreated(view, savedInstanceState) 53 | 54 | swipe_layout.setOnRefreshListener(this) 55 | recycler.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) 56 | } 57 | 58 | override fun onActivityCreated(savedInstanceState: Bundle?) { 59 | super.onActivityCreated(savedInstanceState) 60 | 61 | (component as BusSubcomponent).inject(this) 62 | 63 | adapter.preparedQuery = Queries.prepareCityQuery(dbHelper) 64 | recycler.adapter = adapter 65 | 66 | loaderManager.restartLoader(0, null, this) 67 | 68 | setHasOptionsMenu(true) 69 | } 70 | 71 | override fun onResume() { 72 | super.onResume() 73 | bus.register(this) 74 | adapter.notifyDataSetChanged() 75 | } 76 | 77 | override fun onPause() { 78 | super.onPause() 79 | bus.unregister(this) 80 | } 81 | 82 | override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { 83 | super.onCreateOptionsMenu(menu, inflater) 84 | 85 | inflater!!.inflate(R.menu.main, menu) 86 | 87 | if (Geocoder.isPresent()) { 88 | val addItem = menu!!.findItem(R.id.action_add) 89 | 90 | val searchView = MenuItemCompat.getActionView(addItem) as ArrayAdapterSearchView; 91 | searchView.setOnItemClickListener(AdapterView.OnItemClickListener { adapterView, view, pos, id -> 92 | addItem.collapseActionView(); 93 | searchView.setText(""); 94 | bus.post(AddPlaceEvent(adapterView.getItemAtPosition(pos) as String)); 95 | }); 96 | 97 | val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 98 | searchView.layoutParams = params; 99 | searchView.setAdapter(placesAutoCompleteAdapter); 100 | } else { 101 | longToast("Geocoder is not present") 102 | } 103 | } 104 | 105 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 106 | when (item!!.itemId) { 107 | R.id.action_add -> { 108 | Geocoder.isPresent() 109 | } 110 | 111 | R.id.action_settings -> { 112 | startActivity(Intent(activity, SettingsActivity::class.java)) 113 | return true 114 | } 115 | 116 | R.id.action_debug -> { 117 | startActivity(Intent(activity, DebugActivity::class.java)) 118 | return true 119 | } 120 | } 121 | return super.onOptionsItemSelected(item) 122 | } 123 | 124 | override fun onCreateLoader(i: Int, bundle: Bundle?): Loader? { 125 | try { 126 | return OrmliteCursorLoader(activity, dbHelper.getPlaceDao(), adapter.preparedQuery) 127 | } catch (e: SQLException) { 128 | e.printStackTrace() 129 | } 130 | 131 | return null 132 | } 133 | 134 | override fun onLoadFinished(loader: Loader, cursor: Cursor) = adapter.changeCursor(cursor) 135 | 136 | override fun onLoaderReset(loader: Loader) = adapter.changeCursor(null) 137 | 138 | override fun onRefresh() { 139 | swipe_layout.isRefreshing = true 140 | adapter.clear() 141 | adapter.notifyDataSetChanged() 142 | swipe_layout.isRefreshing = false 143 | } 144 | 145 | @Subscribe fun onUpdateList(event: UpdateListEvent) { 146 | loaderManager.restartLoader(0, null, this) 147 | } 148 | 149 | @Subscribe fun onDeletePlace(event: DeletePlaceEvent) { 150 | if (event.id != null) { 151 | try { 152 | dbHelper.getPlaceDao()!!.deleteById(event.id) 153 | loaderManager.restartLoader(0, null, this) 154 | } catch (e: SQLException) { 155 | e.printStackTrace() 156 | } 157 | } 158 | } 159 | 160 | @Subscribe fun onAddPlace(event: AddPlaceEvent) { 161 | Observables.getGeoForPlace(activity, dbHelper, geoCoder, event.lookupPlace) 162 | .compose(schedulersManager.applySchedulers()) 163 | .subscribeWith { 164 | onNext { bus.post(UpdateListEvent()) } 165 | onError { } 166 | } 167 | } 168 | 169 | companion object { 170 | 171 | public fun newInstance(): WeatherFragment { 172 | return WeatherFragment() 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/net/PlacesApi.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.net 2 | 3 | import com.google.gson.JsonObject 4 | import retrofit.http.GET 5 | import retrofit.http.Query 6 | 7 | /** 8 | * Created by dp on 10/10/14. 9 | */ 10 | interface PlacesApi { 11 | 12 | @GET("/place/autocomplete/json") 13 | fun getAutocomplete(@Query("input") input: String): JsonObject 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/net/WeatherApi.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.net 2 | 3 | import io.dp.weather.app.net.dto.Forecast 4 | import retrofit.http.GET 5 | import retrofit.http.Query 6 | import rx.Observable 7 | 8 | /** 9 | * Created by dp on 08/10/14. 10 | */ 11 | interface WeatherApi { 12 | 13 | @GET("/weather.ashx") 14 | fun getForecast(@Query("q") params: String, @Query("num_of_days") days: Int): Observable 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/net/dto/CurrentCondition.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.net.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import io.dp.weather.app.WeatherIconUrl 5 | 6 | public data class CurrentCondition(@SerializedName("cloudcover") var cloudcover: String?, 7 | @SerializedName("humidity") var humidity: String?, 8 | @SerializedName("observation_time") var observationTime: String?, 9 | @SerializedName("precipMM") var precipMM: String?, 10 | @SerializedName("temp_C") var tempC: String?, 11 | @SerializedName("temp_F") var tempF: String?, 12 | @SerializedName("pressure") var pressure: String?, 13 | @SerializedName("visibility") var visibility: String?, 14 | @SerializedName("weatherCode") var weatherCode: String?, 15 | @SerializedName("weatherDesc") var weatherDesc: List?, 16 | @SerializedName("weatherIconUrl") var weatherIconUrl: List?, 17 | @SerializedName("winddir16Point") var winddir16Point: String?, 18 | @SerializedName("winddirDegree") var winddirDegree: String?, 19 | @SerializedName("windspeedKmph") var windspeedKmph: String?, 20 | @SerializedName("windspeedMiles") var windspeedMiles: String?) 21 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/net/dto/Data.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.net.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class Data(@SerializedName("current_condition") var currentCondition: List?, 6 | @SerializedName("request") var request: List?, 7 | @SerializedName("weather") var weather: List?) 8 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/net/dto/Forecast.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.net.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class Forecast(@SerializedName("data") var data: Data? = null) 6 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/net/dto/Request.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.net.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class Request(@SerializedName("query") var query: String?, 6 | @SerializedName("type") var type: String?) 7 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/net/dto/Weather.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.net.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import io.dp.weather.app.WeatherIconUrl 5 | 6 | data class Weather(@SerializedName("date") var date: String?, 7 | @SerializedName("precipMM") var precipMM: String?, 8 | @SerializedName("tempMaxC") var tempMaxC: String?, 9 | @SerializedName("tempMaxF") var tempMaxF: String?, 10 | @SerializedName("tempMinC") var tempMinC: String?, 11 | @SerializedName("tempMinF") var tempMinF: String?, 12 | @SerializedName("weatherCode") var weatherCode: String?, 13 | @SerializedName("weatherDesc") var weatherDesc: List?, 14 | @SerializedName("weatherIconUrl") var weatherIconUrl: List?, 15 | @SerializedName("winddir16Point") var winddir16Point: String?, 16 | @SerializedName("winddirDegree") var winddirDegree: String?, 17 | @SerializedName("winddirection") var winddirection: String?, 18 | @SerializedName("windspeedKmph") var windspeedKmph: String?, 19 | @SerializedName("windspeedMiles") var windspeedMiles: String?) 20 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/net/dto/WeatherDesc.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.net.dto 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class WeatherDesc(@SerializedName("value") var value: String?) 6 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/net/dto/WeatherIconUrl.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class WeatherIconUrl(@SerializedName("value") var value: String?) 6 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/utils/AsyncBus.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.utils 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import com.squareup.otto.Bus 6 | import com.squareup.otto.ThreadEnforcer 7 | 8 | /** 9 | * Created by dp on 09/10/14. 10 | */ 11 | public class AsyncBus(enforcer: ThreadEnforcer, name: String) : Bus(enforcer, name) { 12 | 13 | private val mainThread = Handler(Looper.getMainLooper()) 14 | 15 | override fun post(event: Any) { 16 | mainThread.post(object : Runnable { 17 | override fun run() { 18 | super@AsyncBus.post(event) 19 | } 20 | }) 21 | } 22 | 23 | public fun postDelayed(event: Any, delayMs: Long) { 24 | mainThread.postDelayed(object : Runnable { 25 | override fun run() { 26 | super@AsyncBus.post(event) 27 | } 28 | }, delayMs) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/utils/MetricsController.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.utils 2 | 3 | import android.content.SharedPreferences 4 | import io.dp.weather.app.Const 5 | import io.dp.weather.app.annotation.ConfigPrefs 6 | import javax.inject.Inject 7 | 8 | /** 9 | * Created by dp on 11/10/14. 10 | */ 11 | public class MetricsController 12 | @Inject 13 | constructor(@ConfigPrefs private val prefs: SharedPreferences) { 14 | val useCelsius: Boolean 15 | get() = prefs.getBoolean(Const.USE_CELCIUS, false) 16 | 17 | val useKmph: Boolean 18 | get() = prefs.getBoolean(Const.USE_KMPH, false) 19 | 20 | val useMmhg: Boolean 21 | get() = prefs.getBoolean(Const.USE_MMHG, false) 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/utils/MyPreferences.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.utils 2 | 3 | import android.content.SharedPreferences 4 | 5 | /** 6 | * Created by dp on 15/11/15. 7 | */ 8 | 9 | fun SharedPreferences.myEdit(func: SharedPreferences.Editor.() -> Array>) : SharedPreferences.Editor { 10 | val editor = edit() 11 | 12 | val pairs = editor.func() 13 | 14 | for ((key, value) in pairs) { 15 | when (value) { 16 | is String -> editor.putString(key, value) 17 | is Set<*> -> { 18 | if (!value.all { it is String }) { 19 | throw IllegalArgumentException("Only Set is supported") 20 | } 21 | editor.putStringSet(key, value as Set) 22 | } 23 | is Int -> editor.putInt(key, value) 24 | is Long -> editor.putLong(key, value) 25 | is Float -> editor.putFloat(key, value) 26 | is Boolean -> editor.putBoolean(key, value) 27 | else -> throw IllegalArgumentException("Unsupported value type: ${value.javaClass}") 28 | } 29 | } 30 | 31 | if (pairs.size > 0) { 32 | editor.apply() 33 | } 34 | 35 | return editor 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/utils/Observables.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.utils 2 | 3 | import android.content.Context 4 | import android.location.Address 5 | import android.location.Geocoder 6 | import android.widget.Toast 7 | import io.dp.weather.app.R 8 | import io.dp.weather.app.db.DatabaseHelper 9 | import io.dp.weather.app.db.table.Place 10 | import org.jetbrains.anko.toast 11 | import rx.Observable 12 | import rx.lang.kotlin.observable 13 | import timber.log.Timber 14 | import java.io.IOException 15 | import java.sql.SQLException 16 | 17 | /** 18 | * Created by dp on 10/10/14. 19 | */ 20 | public object Observables { 21 | 22 | public fun getGeoForPlace(context: Context, 23 | dbHelper: DatabaseHelper, 24 | geocoder: Geocoder, 25 | lookupPlace: String): Observable { 26 | 27 | return observable>() { subscriber -> 28 | try { 29 | val addresses = geocoder.getFromLocationName(lookupPlace, 1) 30 | Timber.v("! got addresses: $addresses") 31 | subscriber.onNext(addresses) 32 | } catch (e: IOException) { 33 | Toast.makeText(context, R.string.cannot_find_geo_for_specified_location, 34 | Toast.LENGTH_SHORT).show() 35 | Timber.e(e, "Cannot find geo for location name") 36 | subscriber.onError(e) 37 | } finally { 38 | subscriber.onCompleted() 39 | } 40 | }.flatMap { addresses -> 41 | observable() { subscriber -> 42 | if (addresses?.size ?: -1 > 0) { 43 | val address = addresses.first() 44 | try { 45 | Timber.v("! Add place to database: $lookupPlace") 46 | val place = Place(lookupPlace, address.latitude, address.longitude) 47 | dbHelper.getPlaceDao()!!.createIfNotExists(place) 48 | 49 | subscriber.onNext(place) 50 | } catch (e: SQLException) { 51 | context.toast(R.string.something_went_wrong_with_adding_new_location) 52 | Timber.e(e, "Cannot add city $address lookupName: $lookupPlace lat ${address.latitude} lon ${address.longitude}") 53 | subscriber.onError(e) 54 | } finally { 55 | subscriber.onCompleted() 56 | } 57 | } else { 58 | Timber.v("! empty addresses: $addresses") 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/utils/WhiteBorderCircleTransformation.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.utils 2 | 3 | import android.graphics.* 4 | 5 | import com.squareup.picasso.Transformation 6 | 7 | /** 8 | * Created by dp on 10/10/14. 9 | */ 10 | public class WhiteBorderCircleTransformation : Transformation { 11 | 12 | private val whitePaint = Paint() 13 | 14 | init { 15 | whitePaint.color = Color.WHITE 16 | whitePaint.isAntiAlias = true 17 | } 18 | 19 | override fun transform(source: Bitmap): Bitmap { 20 | val size = Math.min(source.width, source.height) 21 | 22 | val x = (source.width - size) / 2 23 | val y = (source.height - size) / 2 24 | 25 | val squaredBitmap = Bitmap.createBitmap(source, x, y, size, size) 26 | if (squaredBitmap != source) { 27 | source.recycle() 28 | } 29 | 30 | val bitmap = Bitmap.createBitmap(size, size, source.config) 31 | 32 | val canvas = Canvas(bitmap) 33 | val paint = Paint() 34 | 35 | val shader = BitmapShader(squaredBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) 36 | paint.setShader(shader) 37 | paint.isAntiAlias = true 38 | 39 | val r = size / 2f 40 | val margin = r - 2.0f 41 | 42 | canvas.drawCircle(r, r, r, whitePaint) 43 | canvas.drawCircle(r, r, margin, paint) 44 | 45 | squaredBitmap.recycle() 46 | return bitmap 47 | } 48 | 49 | override fun key(): String { 50 | return "white_border_circle" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/widget/ArrayAdapterSearchView.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.widget 2 | 3 | import android.content.Context 4 | import android.support.v4.widget.CursorAdapter 5 | import android.support.v7.widget.SearchView 6 | import android.util.AttributeSet 7 | import android.widget.AdapterView 8 | import android.widget.ArrayAdapter 9 | 10 | class ArrayAdapterSearchView : SearchView { 11 | 12 | private lateinit var searchAutoComplete: SearchView.SearchAutoComplete 13 | 14 | constructor(context: Context) : super(context) { 15 | initialize() 16 | } 17 | 18 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { 19 | initialize() 20 | } 21 | 22 | fun initialize() { 23 | searchAutoComplete = findViewById( 24 | android.support.v7.appcompat.R.id.search_src_text) as SearchView.SearchAutoComplete 25 | this.setAdapter(null) 26 | this.setOnItemClickListener(null) 27 | } 28 | 29 | override fun setSuggestionsAdapter(adapter: CursorAdapter) { 30 | // don't let anyone touch this 31 | } 32 | 33 | fun setOnItemClickListener(listener: AdapterView.OnItemClickListener?) { 34 | searchAutoComplete.onItemClickListener = listener 35 | } 36 | 37 | fun setAdapter(adapter: ArrayAdapter<*>?) = searchAutoComplete.setAdapter>(adapter) 38 | 39 | fun setText(text: String) = searchAutoComplete.setText(text) 40 | } -------------------------------------------------------------------------------- /app/src/main/java/io/dp/weather/app/widget/WeatherFor5DaysView.kt: -------------------------------------------------------------------------------- 1 | package io.dp.weather.app.widget 2 | 3 | import android.content.Context 4 | import android.text.TextUtils 5 | import android.text.format.DateUtils 6 | import android.util.AttributeSet 7 | import android.view.LayoutInflater 8 | import android.widget.ImageView 9 | import android.widget.LinearLayout 10 | import android.widget.TextView 11 | import com.squareup.picasso.Picasso 12 | import com.squareup.picasso.Transformation 13 | import io.dp.weather.app.R 14 | import io.dp.weather.app.net.dto.Weather 15 | import kotlinx.android.synthetic.view_weather_for_week.view.* 16 | import java.sql.Date 17 | 18 | public class WeatherFor5DaysView : LinearLayout { 19 | 20 | lateinit var dayNameViews: List 21 | lateinit var dayViews: List 22 | lateinit var tempViews: List 23 | 24 | lateinit var transformation: Transformation 25 | 26 | var celsius: String? = null 27 | var fahrenheit: String? = null 28 | 29 | public constructor(context: Context) : super(context) { 30 | init(context) 31 | } 32 | 33 | public constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { 34 | init(context) 35 | } 36 | 37 | private fun init(context: Context) { 38 | LayoutInflater.from(context).inflate(R.layout.view_weather_for_week, this, true) 39 | 40 | celsius = context.getString(R.string.celcius) 41 | fahrenheit = context.getString(R.string.fahrenheit) 42 | } 43 | 44 | override fun onFinishInflate() { 45 | super.onFinishInflate() 46 | 47 | dayNameViews = arrayListOf(day_name_1, day_name_2, day_name_3, day_name_4, day_name_5) 48 | dayViews = arrayListOf(day_1, day_2, day_3, day_4, day_5) 49 | tempViews = arrayListOf(temp_1, temp_2, temp_3, temp_4, temp_5) 50 | } 51 | 52 | public fun setWeatherForWeek(weatherList: List, useCelsius: Boolean, transformation: Transformation) { 53 | this.transformation = transformation 54 | 55 | for (i in weatherList.indices) { 56 | val v = dayViews[i] 57 | val weather = weatherList[i] 58 | 59 | try { 60 | val date = Date.valueOf(weather.date) 61 | val weekDay = DateUtils.formatDateTime(context, date.time, DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_ABBREV_WEEKDAY) 62 | dayNameViews[i].text = weekDay 63 | } catch (e: IllegalArgumentException) { 64 | dayNameViews[i].text = "" 65 | } 66 | 67 | if (useCelsius) { 68 | tempViews[i].text = "${weather.tempMinC}-${weather.tempMaxC}${context!!.getString(R.string.celcius)}" 69 | } else { 70 | tempViews[i].text = "${weather.tempMinF}-${weather.tempMaxF}${context!!.getString(R.string.fahrenheit)}" 71 | } 72 | 73 | val urls = weather.weatherIconUrl 74 | if (urls?.isNotEmpty() ?: false) { 75 | val url = urls?.get(0) 76 | if (!TextUtils.isEmpty(url?.value)) { 77 | Picasso.with(context).load(url?.value).transform(transformation).into(v) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ico_menu_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-hdpi/ico_menu_small.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ico_menu_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-mdpi/ico_menu_small.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ico_menu_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-xhdpi/ico_menu_small.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ico_menu_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-xxhdpi/ico_menu_small.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpolishuk/weather-android-kotlin/1d7b0ec1d3b5132fc9a1caa8e335c9ad6143ddef/app/src/main/res/drawable-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/card.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_main.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_weather.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 12 | 13 | 18 | 19 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_city_weather.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 12 | 13 | 17 | 18 | 22 | 23 | 25 | 26 | 35 | 36 |