Browse Source

TeddeMacBook-Pro.local push @ 2017-08-16_10-00-55

Ted 6 years ago
parent
commit
1ef22b66f6
43 changed files with 975 additions and 121 deletions
  1. 3 0
      .idea/misc.xml
  2. 6 0
      .idea/vcs.xml
  3. 29 9
      app/build.gradle
  4. 17 1
      app/src/main/AndroidManifest.xml
  5. 0 52
      app/src/main/java/com/ted/weather/MainActivity.java
  6. 36 0
      app/src/main/java/com/ted/weather/data/db/DbClasses.kt
  7. 25 0
      app/src/main/java/com/ted/weather/data/db/DbDataMapper.kt
  8. 44 0
      app/src/main/java/com/ted/weather/data/db/ForecastDb.kt
  9. 39 0
      app/src/main/java/com/ted/weather/data/db/ForecastDbHelper.kt
  10. 19 0
      app/src/main/java/com/ted/weather/data/db/Tables.kt
  11. 21 0
      app/src/main/java/com/ted/weather/data/server/ForecastByZipCodeRequest.kt
  12. 18 0
      app/src/main/java/com/ted/weather/data/server/ForecastServer.kt
  13. 17 0
      app/src/main/java/com/ted/weather/data/server/ServerClasses.kt
  14. 27 0
      app/src/main/java/com/ted/weather/data/server/ServerDataMapper.kt
  15. 5 0
      app/src/main/java/com/ted/weather/domain/commands/Command.kt
  16. 12 0
      app/src/main/java/com/ted/weather/domain/commands/RequestDayForecastCommand.kt
  17. 16 0
      app/src/main/java/com/ted/weather/domain/commands/RequestForecastCommand.kt
  18. 12 0
      app/src/main/java/com/ted/weather/domain/datasource/ForecastDataSource.kt
  19. 28 0
      app/src/main/java/com/ted/weather/domain/datasource/ForecastProvider.kt
  20. 12 0
      app/src/main/java/com/ted/weather/domain/model/DomainClasses.kt
  21. 14 0
      app/src/main/java/com/ted/weather/extensions/CollectionsExtensions.kt
  22. 6 0
      app/src/main/java/com/ted/weather/extensions/ContextExtensions.kt
  23. 21 0
      app/src/main/java/com/ted/weather/extensions/DatabaseExtensions.kt
  24. 65 0
      app/src/main/java/com/ted/weather/extensions/DelegatesExtensions.kt
  25. 9 0
      app/src/main/java/com/ted/weather/extensions/ExtensionUtils.kt
  26. 20 0
      app/src/main/java/com/ted/weather/extensions/ViewExtensions.kt
  27. 16 0
      app/src/main/java/com/ted/weather/ui/App.kt
  28. 60 0
      app/src/main/java/com/ted/weather/ui/activity/DetailActivity.kt
  29. 52 0
      app/src/main/java/com/ted/weather/ui/activity/MainActivity.kt
  30. 40 0
      app/src/main/java/com/ted/weather/ui/activity/SettingsActivity.kt
  31. 49 0
      app/src/main/java/com/ted/weather/ui/activity/ToolbarManager.kt
  32. 42 0
      app/src/main/java/com/ted/weather/ui/adapter/ForecastListAdapter.kt
  33. 63 0
      app/src/main/res/layout/activity_detail.xml
  34. 9 28
      app/src/main/res/layout/activity_main.xml
  35. 31 0
      app/src/main/res/layout/activity_settings.xml
  36. 0 21
      app/src/main/res/layout/content_main.xml
  37. 64 0
      app/src/main/res/layout/item_forecast.xml
  38. 11 0
      app/src/main/res/layout/toolbar.xml
  39. 1 1
      app/src/main/res/menu/menu_main.xml
  40. 5 0
      app/src/main/res/values/dimens.xml
  41. 2 0
      app/src/main/res/values/strings.xml
  42. 6 8
      build.gradle
  43. 3 1
      version.gradle

+ 3 - 0
.idea/misc.xml

@@ -1,5 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
+  <component name="EntryPointsManager">
+    <entry_points version="2.0" />
+  </component>
   <component name="MarkdownProjectSettings">
     <PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.0" maxImageWidth="0" showGitHubPageIfSynced="false" allowBrowsingInPreview="false" synchronizePreviewPosition="true" highlightPreviewType="NONE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="false" showSelectionInPreview="true">
       <PanelProvider>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 29 - 9
app/build.gradle

@@ -1,14 +1,16 @@
 apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
 
 android {
-    compileSdkVersion 26
-    buildToolsVersion "26.0.1"
+    compileSdkVersion getProject().compileSdkVersion
+    buildToolsVersion getProject().buildToolsVersion
     defaultConfig {
         applicationId "com.ted.weather"
-        minSdkVersion 16
-        targetSdkVersion 26
-        versionCode 1
-        versionName "1.0"
+        minSdkVersion getProject().minSdkVersion
+        targetSdkVersion getProject().targetSdkVersion
+        versionCode getProject().versionCode
+        versionName getProject().versionName
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
     }
     buildTypes {
@@ -24,8 +26,26 @@ dependencies {
     androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
         exclude group: 'com.android.support', module: 'support-annotations'
     })
-    compile 'com.android.support:appcompat-v7:26.+'
-    compile 'com.android.support.constraint:constraint-layout:1.0.2'
-    compile 'com.android.support:design:26.+'
+
+    compile "com.google.code.gson:gson:2.8.1"
+    compile getProject().appcompatV7
+    compile getProject().constraintLayout
+    compile getProject().design
+    compile getProject().recycleView
     testCompile 'junit:junit:4.12'
+    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+    compile "org.jetbrains.anko:anko-commons:$anko_version"
+    compile "org.jetbrains.anko:anko-sqlite:$anko_version"
+    compile "org.jetbrains.anko:anko-coroutines:$anko_version"
+    compile "com.squareup.picasso:picasso:2.5.2"
+}
+
+kotlin {
+    experimental {
+        coroutines 'enable'
+    }
+}
+
+repositories {
+    mavenCentral()
 }

+ 17 - 1
app/src/main/AndroidManifest.xml

@@ -1,8 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.ted.weather">
+    <uses-permission android:name="android.permission.INTERNET"/>
 
     <application
+        android:name=".ui.App"
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/app_name"
@@ -10,7 +12,7 @@
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
         <activity
-            android:name=".MainActivity"
+            android:name=".ui.activity.MainActivity"
             android:label="@string/app_name"
             android:theme="@style/AppTheme.NoActionBar">
             <intent-filter>
@@ -19,6 +21,20 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+
+        <activity
+            android:theme="@style/AppTheme.NoActionBar"
+            android:name=".ui.activity.DetailActivity"
+            android:parentActivityName=".ui.activity.MainActivity">
+            <meta-data
+                android:name="android.support.PARENT_ACTIVITY"
+                android:value="com.ted.weather.ui.activity.MainActivity"/>
+        </activity>
+        <activity
+            android:theme="@style/AppTheme.NoActionBar"
+            android:name=".ui.activity.SettingsActivity"
+            android:label="@string/setting"/>
+
     </application>
 
 </manifest>

+ 0 - 52
app/src/main/java/com/ted/weather/MainActivity.java

@@ -1,52 +0,0 @@
-package com.ted.weather;
-
-import android.os.Bundle;
-import android.support.design.widget.FloatingActionButton;
-import android.support.design.widget.Snackbar;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.Toolbar;
-import android.view.View;
-import android.view.Menu;
-import android.view.MenuItem;
-
-public class MainActivity extends AppCompatActivity {
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_main);
-        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
-        setSupportActionBar(toolbar);
-
-        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
-        fab.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
-                        .setAction("Action", null).show();
-            }
-        });
-    }
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        // Inflate the menu; this adds items to the action bar if it is present.
-        getMenuInflater().inflate(R.menu.menu_main, menu);
-        return true;
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        // Handle action bar item clicks here. The action bar will
-        // automatically handle clicks on the Home/Up button, so long
-        // as you specify a parent activity in AndroidManifest.xml.
-        int id = item.getItemId();
-
-        //noinspection SimplifiableIfStatement
-        if (id == R.id.action_settings) {
-            return true;
-        }
-
-        return super.onOptionsItemSelected(item);
-    }
-}

+ 36 - 0
app/src/main/java/com/ted/weather/data/db/DbClasses.kt

@@ -0,0 +1,36 @@
+package com.ted.weather.data.db
+
+import java.util.*
+
+class CityForecast(val map: MutableMap<String, Any?>, val dailyForecast: List<DayForecast>) {
+    var _id: Long by map
+    var city: String by map
+    var country: String by map
+
+    constructor(id: Long, city: String, country: String, dailyForecast: List<DayForecast>)
+            : this(HashMap(), dailyForecast) {
+        this._id = id
+        this.city = city
+        this.country = country
+    }
+}
+
+class DayForecast(var map: MutableMap<String, Any?>) {
+    var _id: Long by map
+    var date: Long by map
+    var description: String by map
+    var high: Int by map
+    var low: Int by map
+    var iconUrl: String by map
+    var cityId: Long by map
+
+    constructor(date: Long, description: String, high: Int, low: Int, iconUrl: String, cityId: Long)
+            : this(HashMap()) {
+        this.date = date
+        this.description = description
+        this.high = high
+        this.low = low
+        this.iconUrl = iconUrl
+        this.cityId = cityId
+    }
+}

+ 25 - 0
app/src/main/java/com/ted/weather/data/db/DbDataMapper.kt

@@ -0,0 +1,25 @@
+package com.ted.weather.data.db
+
+import com.ted.weather.domain.model.Forecast
+import com.ted.weather.domain.model.ForecastList
+
+class DbDataMapper {
+
+    fun convertFromDomain(forecast: ForecastList) = with(forecast) {
+        val daily = dailyForecast.map { convertDayFromDomain(id, it) }
+        CityForecast(id, city, country, daily)
+    }
+
+    private fun convertDayFromDomain(cityId: Long, forecast: Forecast) = with(forecast) {
+        DayForecast(date, description, high, low, iconUrl, cityId)
+    }
+
+    fun convertToDomain(forecast: CityForecast) = with(forecast) {
+        val daily = dailyForecast.map { convertDayToDomain(it) }
+        ForecastList(_id, city, country, daily)
+    }
+
+    fun convertDayToDomain(dayForecast: DayForecast) = with(dayForecast) {
+        Forecast(_id, date, description, high, low, iconUrl)
+    }
+}

+ 44 - 0
app/src/main/java/com/ted/weather/data/db/ForecastDb.kt

@@ -0,0 +1,44 @@
+package com.ted.weather.data.db
+
+import com.ted.weather.domain.datasource.ForecastDataSource
+import com.ted.weather.domain.model.ForecastList
+import com.ted.weather.extensions.*
+import org.jetbrains.anko.db.insert
+import org.jetbrains.anko.db.select
+import java.util.*
+
+class ForecastDb(val forecastDbHelper: ForecastDbHelper = ForecastDbHelper.instance,
+                 val dataMapper: DbDataMapper = DbDataMapper()) : ForecastDataSource {
+
+    override fun requestForecastByZipCode(zipCode: Long, date: Long) = forecastDbHelper.use {
+
+        val dailyRequest = "${DayForecastTable.CITY_ID} = ? AND ${DayForecastTable.DATE} >= ?"
+        val dailyForecast = select(DayForecastTable.NAME)
+                .whereSimple(dailyRequest, zipCode.toString(), date.toString())
+                .parseList { DayForecast(HashMap(it)) }
+
+        val city = select(CityForecastTable.NAME)
+                .whereSimple("${CityForecastTable.ID} = ?", zipCode.toString())
+                .parseOpt { CityForecast(HashMap(it), dailyForecast) }
+
+        city?.let { dataMapper.convertToDomain(it) }
+    }
+
+    override fun requestDayForecast(id: Long) = forecastDbHelper.use {
+        val forecast = select(DayForecastTable.NAME).byId(id).
+                parseOpt { DayForecast(HashMap(it)) }
+
+        forecast?.let { dataMapper.convertDayToDomain(it) }
+    }
+
+    fun saveForecast(forecast: ForecastList) = forecastDbHelper.use {
+
+        clear(CityForecastTable.NAME)
+        clear(DayForecastTable.NAME)
+
+        with(dataMapper.convertFromDomain(forecast)) {
+            insert(CityForecastTable.NAME, *map.toVarargArray())
+            dailyForecast.forEach { insert(DayForecastTable.NAME, *it.map.toVarargArray()) }
+        }
+    }
+}

+ 39 - 0
app/src/main/java/com/ted/weather/data/db/ForecastDbHelper.kt

@@ -0,0 +1,39 @@
+package com.ted.weather.data.db
+
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import com.ted.weather.ui.App
+import org.jetbrains.anko.db.*
+
+class ForecastDbHelper(ctx: Context = App.instance) : ManagedSQLiteOpenHelper(ctx,
+        ForecastDbHelper.DB_NAME, null, ForecastDbHelper.DB_VERSION) {
+
+    companion object {
+        val DB_NAME = "forecast.db"
+        val DB_VERSION = 1
+        val instance by lazy { ForecastDbHelper() }
+    }
+
+    override fun onCreate(db: SQLiteDatabase) {
+        db.createTable(CityForecastTable.NAME, true,
+                CityForecastTable.ID to INTEGER + PRIMARY_KEY,
+                CityForecastTable.CITY to TEXT,
+                CityForecastTable.COUNTRY to TEXT)
+
+        db.createTable(DayForecastTable.NAME, true,
+                DayForecastTable.ID to INTEGER + PRIMARY_KEY,
+                DayForecastTable.DATE to INTEGER,
+                DayForecastTable.DESCRIPTION to TEXT,
+                DayForecastTable.HIGH to INTEGER,
+                DayForecastTable.LOW to INTEGER,
+                DayForecastTable.ICON_URL to TEXT,
+                DayForecastTable.CITY_ID to INTEGER)
+    }
+
+    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
+        db.dropTable(CityForecastTable.NAME, true)
+        db.dropTable(DayForecastTable.NAME, true)
+        onCreate(db)
+    }
+}
+

+ 19 - 0
app/src/main/java/com/ted/weather/data/db/Tables.kt

@@ -0,0 +1,19 @@
+package com.ted.weather.data.db
+
+object CityForecastTable {
+    val NAME = "CityForecast"
+    val ID = "_id"
+    val CITY = "city"
+    val COUNTRY = "country"
+}
+
+object DayForecastTable {
+    val NAME = "DayForecast"
+    val ID = "_id"
+    val DATE = "date"
+    val DESCRIPTION = "description"
+    val HIGH = "high"
+    val LOW = "low"
+    val ICON_URL = "iconUrl"
+    val CITY_ID = "cityId"
+}

+ 21 - 0
app/src/main/java/com/ted/weather/data/server/ForecastByZipCodeRequest.kt

@@ -0,0 +1,21 @@
+package com.ted.weather.data.server
+
+import android.util.Log
+import com.google.gson.Gson
+import java.net.URL
+
+class ForecastByZipCodeRequest(val zipCode: Long, val gson: Gson = Gson()) {
+
+    companion object {
+        private val APP_ID = "cacfc04fbae471861e7d282149e2aa0f"
+        private val URL = "http://api.openweathermap.org/data/2.5/forecast/daily?mode=json&units=metric&cnt=7"
+        private val COMPLETE_URL = "$URL&APPID=$APP_ID&q="
+    }
+
+    fun execute(): ForecastResult {
+        Log.d("xiongwei",COMPLETE_URL + zipCode)
+        val forecastJsonStr = URL(COMPLETE_URL + zipCode).readText()
+        Log.d("xiongwei",forecastJsonStr)
+        return gson.fromJson(forecastJsonStr, ForecastResult::class.java)
+    }
+}

+ 18 - 0
app/src/main/java/com/ted/weather/data/server/ForecastServer.kt

@@ -0,0 +1,18 @@
+package com.ted.weather.data.server
+
+import com.ted.weather.data.db.ForecastDb
+import com.ted.weather.domain.datasource.ForecastDataSource
+import com.ted.weather.domain.model.ForecastList
+
+class ForecastServer(val dataMapper: ServerDataMapper = ServerDataMapper(),
+                     val forecastDb: ForecastDb = ForecastDb()) : ForecastDataSource {
+
+    override fun requestForecastByZipCode(zipCode: Long, date: Long): ForecastList? {
+        val result = ForecastByZipCodeRequest(zipCode).execute()
+        val converted = dataMapper.convertToDomain(zipCode, result)
+        forecastDb.saveForecast(converted)
+        return forecastDb.requestForecastByZipCode(zipCode, date)
+    }
+
+    override fun requestDayForecast(id: Long) = throw UnsupportedOperationException()
+}

+ 17 - 0
app/src/main/java/com/ted/weather/data/server/ServerClasses.kt

@@ -0,0 +1,17 @@
+package com.ted.weather.data.server
+
+data class ForecastResult(val city: City, val list: List<Forecast>)
+
+data class City(val id: Long, val name: String, val coord: Coordinates, val country: String,
+        val population: Int)
+
+data class Coordinates(val lon: Float, val lat: Float)
+
+data class Forecast(val dt: Long, val temp: Temperature, val pressure: Float, val humidity: Int,
+        val weather: List<Weather>, val speed: Float, val deg: Int, val clouds: Int,
+        val rain: Float)
+
+data class Temperature(val day: Float, val min: Float, val max: Float, val night: Float,
+        val eve: Float, val morn: Float)
+
+data class Weather(val id: Long, val main: String, val description: String, val icon: String)

+ 27 - 0
app/src/main/java/com/ted/weather/data/server/ServerDataMapper.kt

@@ -0,0 +1,27 @@
+package com.ted.weather.data.server
+
+import com.ted.weather.domain.model.ForecastList
+import java.util.*
+import java.util.concurrent.TimeUnit
+import com.ted.weather.domain.model.Forecast as ModelForecast
+
+class ServerDataMapper {
+
+    fun convertToDomain(zipCode: Long, forecast: ForecastResult) = with(forecast) {
+        ForecastList(zipCode, city.name, city.country, convertForecastListToDomain(list))
+    }
+
+    private fun convertForecastListToDomain(list: List<Forecast>): List<ModelForecast> {
+        return list.mapIndexed { i, forecast ->
+            val dt = Calendar.getInstance().timeInMillis + TimeUnit.DAYS.toMillis(i.toLong())
+            convertForecastItemToDomain(forecast.copy(dt = dt))
+        }
+    }
+
+    private fun convertForecastItemToDomain(forecast: Forecast) = with(forecast) {
+        ModelForecast(-1, dt, weather[0].description, temp.max.toInt(), temp.min.toInt(),
+                generateIconUrl(weather[0].icon))
+    }
+
+    private fun generateIconUrl(iconCode: String) = "http://openweathermap.org/img/w/$iconCode.png"
+}

+ 5 - 0
app/src/main/java/com/ted/weather/domain/commands/Command.kt

@@ -0,0 +1,5 @@
+package com.ted.weather.domain.commands
+
+interface Command<out T> {
+    fun execute(): T
+}

+ 12 - 0
app/src/main/java/com/ted/weather/domain/commands/RequestDayForecastCommand.kt

@@ -0,0 +1,12 @@
+package com.ted.weather.domain.commands
+
+import com.ted.weather.domain.datasource.ForecastProvider
+import com.ted.weather.domain.model.Forecast
+
+class RequestDayForecastCommand(
+        val id: Long,
+        val forecastProvider: ForecastProvider = ForecastProvider()) :
+        Command<Forecast> {
+
+    override fun execute() = forecastProvider.requestForecast(id)
+}

+ 16 - 0
app/src/main/java/com/ted/weather/domain/commands/RequestForecastCommand.kt

@@ -0,0 +1,16 @@
+package com.ted.weather.domain.commands
+
+import com.ted.weather.domain.datasource.ForecastProvider
+import com.ted.weather.domain.model.ForecastList
+
+class RequestForecastCommand(
+        val zipCode: Long,
+        val forecastProvider: ForecastProvider = ForecastProvider()) :
+        Command<ForecastList> {
+
+    companion object {
+        val DAYS = 7
+    }
+
+    override fun execute() = forecastProvider.requestByZipCode(zipCode, DAYS)
+}

+ 12 - 0
app/src/main/java/com/ted/weather/domain/datasource/ForecastDataSource.kt

@@ -0,0 +1,12 @@
+package com.ted.weather.domain.datasource
+
+import com.ted.weather.domain.model.Forecast
+import com.ted.weather.domain.model.ForecastList
+
+interface ForecastDataSource {
+
+    fun requestForecastByZipCode(zipCode: Long, date: Long): ForecastList?
+
+    fun requestDayForecast(id: Long): Forecast?
+
+}

+ 28 - 0
app/src/main/java/com/ted/weather/domain/datasource/ForecastProvider.kt

@@ -0,0 +1,28 @@
+package com.ted.weather.domain.datasource
+
+import com.ted.weather.data.db.ForecastDb
+import com.ted.weather.data.server.ForecastServer
+import com.ted.weather.domain.model.Forecast
+import com.ted.weather.domain.model.ForecastList
+import com.ted.weather.extensions.firstResult
+
+class ForecastProvider(val sources: List<ForecastDataSource> = ForecastProvider.SOURCES) {
+
+    companion object {
+        val DAY_IN_MILLIS = 1000 * 60 * 60 * 24
+        val SOURCES by lazy { listOf(ForecastDb(), ForecastServer()) }
+    }
+
+    fun requestByZipCode(zipCode: Long, days: Int): ForecastList = requestToSources {
+        val res = it.requestForecastByZipCode(zipCode, todayTimeSpan())
+        if (res != null && res.size >= days) res else null
+    }
+
+    fun requestForecast(id: Long): Forecast = requestToSources { it.requestDayForecast(id) }
+
+    private fun todayTimeSpan() = System.currentTimeMillis() / DAY_IN_MILLIS * DAY_IN_MILLIS
+
+    private fun <T : Any> requestToSources(
+            f: (ForecastDataSource) -> T?): T = sources.firstResult { f(it) }
+
+}

+ 12 - 0
app/src/main/java/com/ted/weather/domain/model/DomainClasses.kt

@@ -0,0 +1,12 @@
+package com.ted.weather.domain.model
+
+data class ForecastList(val id: Long, val city: String, val country: String,
+                        val dailyForecast: List<Forecast>) {
+
+    val size: Int get() = dailyForecast.size
+
+    operator fun get(position: Int) = dailyForecast[position]
+}
+
+data class Forecast(val id: Long, val date: Long, val description: String, val high: Int,
+                    val low: Int, val iconUrl: String)

+ 14 - 0
app/src/main/java/com/ted/weather/extensions/CollectionsExtensions.kt

@@ -0,0 +1,14 @@
+package com.ted.weather.extensions
+
+import java.util.*
+
+fun <K, V : Any> Map<K, V?>.toVarargArray(): Array<out Pair<K, V>> =
+        map({ Pair(it.key, it.value!!) }).toTypedArray()
+
+inline fun <T, R : Any> Iterable<T>.firstResult(predicate: (T) -> R?): R {
+    for (element in this) {
+        val result = predicate(element)
+        if (result != null) return result
+    }
+    throw NoSuchElementException("No element matching predicate was found.")
+}

+ 6 - 0
app/src/main/java/com/ted/weather/extensions/ContextExtensions.kt

@@ -0,0 +1,6 @@
+package com.ted.weather.extensions
+
+import android.content.Context
+import android.support.v4.content.ContextCompat
+
+fun Context.color(res: Int): Int = ContextCompat.getColor(this, res)

+ 21 - 0
app/src/main/java/com/ted/weather/extensions/DatabaseExtensions.kt

@@ -0,0 +1,21 @@
+package com.ted.weather.extensions
+
+import android.database.sqlite.SQLiteDatabase
+import org.jetbrains.anko.db.MapRowParser
+import org.jetbrains.anko.db.SelectQueryBuilder
+
+fun <T : Any> SelectQueryBuilder.parseList(parser: (Map<String, Any?>) -> T): List<T> =
+        parseList(object : MapRowParser<T> {
+            override fun parseRow(columns: Map<String, Any?>): T = parser(columns)
+        })
+
+fun <T : Any> SelectQueryBuilder.parseOpt(parser: (Map<String, Any?>) -> T): T? =
+        parseOpt(object : MapRowParser<T> {
+            override fun parseRow(columns: Map<String, Any?>): T = parser(columns)
+        })
+
+fun SQLiteDatabase.clear(tableName: String) {
+    execSQL("delete from $tableName")
+}
+
+fun SelectQueryBuilder.byId(id: Long) = whereSimple("_id = ?", id.toString())

+ 65 - 0
app/src/main/java/com/ted/weather/extensions/DelegatesExtensions.kt

@@ -0,0 +1,65 @@
+package com.ted.weather.extensions
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.SharedPreferences
+import kotlin.reflect.KProperty
+
+object DelegatesExt {
+    fun <T> notNullSingleValue() = NotNullSingleValueVar<T>()
+    fun <T> preference(context: Context, name: String,
+            default: T) = Preference(context, name, default)
+}
+
+class NotNullSingleValueVar<T> {
+
+    private var value: T? = null
+
+    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
+        return value ?: throw IllegalStateException("${property.name} not initialized")
+    }
+
+    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+        this.value = if (this.value == null) value
+        else throw IllegalStateException("${property.name} already initialized")
+    }
+}
+
+class Preference<T>(val context: Context, val name: String, val default: T) {
+
+    val prefs: SharedPreferences by lazy { context.getSharedPreferences("default", Context.MODE_PRIVATE) }
+
+    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
+        return findPreference(name, default)
+    }
+
+    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
+        putPreference(name, value)
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    private fun findPreference(name: String, default: T): T = with(prefs) {
+        val res: Any = when (default) {
+            is Long -> getLong(name, default)
+            is String -> getString(name, default)
+            is Int -> getInt(name, default)
+            is Boolean -> getBoolean(name, default)
+            is Float -> getFloat(name, default)
+            else -> throw IllegalArgumentException("This type can be saved into Preferences")
+        }
+
+        res as T
+    }
+
+    @SuppressLint("CommitPrefEdits")
+    private fun putPreference(name: String, value: T) = with(prefs.edit()) {
+        when (value) {
+            is Long -> putLong(name, value)
+            is String -> putString(name, value)
+            is Int -> putInt(name, value)
+            is Boolean -> putBoolean(name, value)
+            is Float -> putFloat(name, value)
+            else -> throw IllegalArgumentException("This type can't be saved into Preferences")
+        }.apply()
+    }
+}

+ 9 - 0
app/src/main/java/com/ted/weather/extensions/ExtensionUtils.kt

@@ -0,0 +1,9 @@
+package com.ted.weather.extensions
+
+import java.text.DateFormat
+import java.util.*
+
+fun Long.toDateString(dateFormat: Int = DateFormat.MEDIUM): String {
+    val df = DateFormat.getDateInstance(dateFormat, Locale.getDefault())
+    return df.format(this)
+}

+ 20 - 0
app/src/main/java/com/ted/weather/extensions/ViewExtensions.kt

@@ -0,0 +1,20 @@
+package com.ted.weather.extensions
+
+import android.content.Context
+import android.view.View
+import android.widget.TextView
+
+val View.ctx: Context
+    get() = context
+
+var TextView.textColor: Int
+    get() = currentTextColor
+    set(v) = setTextColor(v)
+
+fun View.slideExit() {
+    if (translationY == 0f) animate().translationY(-height.toFloat())
+}
+
+fun View.slideEnter() {
+    if (translationY < 0f) animate().translationY(0f)
+}

+ 16 - 0
app/src/main/java/com/ted/weather/ui/App.kt

@@ -0,0 +1,16 @@
+package com.ted.weather.ui
+
+import android.app.Application
+import com.ted.weather.extensions.DelegatesExt
+
+class App : Application() {
+
+    companion object {
+        var instance: App by DelegatesExt.notNullSingleValue()
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        instance = this
+    }
+}

+ 60 - 0
app/src/main/java/com/ted/weather/ui/activity/DetailActivity.kt

@@ -0,0 +1,60 @@
+package com.ted.weather.ui.activity
+
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.widget.Toolbar
+import android.widget.TextView
+import com.squareup.picasso.Picasso
+import com.ted.weather.R
+import com.ted.weather.domain.commands.RequestDayForecastCommand
+import com.ted.weather.domain.model.Forecast
+import com.ted.weather.extensions.color
+import com.ted.weather.extensions.textColor
+import com.ted.weather.extensions.toDateString
+import kotlinx.android.synthetic.main.activity_detail.*
+import kotlinx.coroutines.experimental.android.UI
+import kotlinx.coroutines.experimental.async
+import org.jetbrains.anko.coroutines.experimental.bg
+import org.jetbrains.anko.ctx
+import org.jetbrains.anko.find
+import java.text.DateFormat
+
+class DetailActivity : AppCompatActivity(), ToolbarManager {
+
+    override val toolbar by lazy { find<Toolbar>(R.id.toolbar) }
+
+    companion object {
+        val ID = "DetailActivity:id"
+        val CITY_NAME = "DetailActivity:cityName"
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_detail)
+        initToolbar()
+
+        toolbarTitle = intent.getStringExtra(CITY_NAME)
+        enableHomeAsUp { onBackPressed() }
+
+        async(UI) {
+            val result = bg { RequestDayForecastCommand(intent.getLongExtra(ID, -1)).execute() }
+            bindForecast(result.await())
+        }
+    }
+
+    private fun bindForecast(forecast: Forecast) = with(forecast) {
+        Picasso.with(ctx).load(iconUrl).into(icon)
+        toolbar.subtitle = date.toDateString(DateFormat.FULL)
+        weatherDescription.text = description
+        bindWeather(high to maxTemperature, low to minTemperature)
+    }
+
+    private fun bindWeather(vararg views: Pair<Int, TextView>) = views.forEach {
+        it.second.text = "${it.first}º"
+        it.second.textColor = color(when (it.first) {
+            in -50..0 -> android.R.color.holo_red_dark
+            in 0..15 -> android.R.color.holo_orange_dark
+            else -> android.R.color.holo_green_dark
+        })
+    }
+}

+ 52 - 0
app/src/main/java/com/ted/weather/ui/activity/MainActivity.kt

@@ -0,0 +1,52 @@
+package com.ted.weather.ui.activity
+
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.Toolbar
+import com.ted.weather.R
+import com.ted.weather.domain.commands.RequestForecastCommand
+import com.ted.weather.domain.model.ForecastList
+import com.ted.weather.extensions.DelegatesExt
+import com.ted.weather.ui.adapter.ForecastListAdapter
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.coroutines.experimental.android.UI
+import kotlinx.coroutines.experimental.async
+import org.jetbrains.anko.coroutines.experimental.bg
+import org.jetbrains.anko.find
+import org.jetbrains.anko.startActivity
+
+class MainActivity : AppCompatActivity(), ToolbarManager {
+
+    val zipCode: Long by DelegatesExt.preference(this, SettingsActivity.ZIP_CODE,
+            SettingsActivity.DEFAULT_ZIP)
+    override val toolbar by lazy { find<Toolbar>(R.id.toolbar) }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+        initToolbar()
+
+        forecastList.layoutManager = LinearLayoutManager(this)
+        attachToScroll(forecastList)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        loadForecast()
+    }
+
+    private fun loadForecast() = async(UI) {
+        val result = bg { RequestForecastCommand(zipCode).execute() }
+        updateUI(result.await())
+    }
+
+    private fun updateUI(weekForecast: ForecastList) {
+        val adapter = ForecastListAdapter(weekForecast) {
+            startActivity<DetailActivity>(DetailActivity.ID to it.id,
+                    DetailActivity.CITY_NAME to weekForecast.city)
+        }
+        forecastList.adapter = adapter
+        toolbarTitle = "${weekForecast.city} (${weekForecast.country})"
+    }
+}

+ 40 - 0
app/src/main/java/com/ted/weather/ui/activity/SettingsActivity.kt

@@ -0,0 +1,40 @@
+package com.ted.weather.ui.activity
+
+import android.os.Bundle
+import android.support.v7.app.AppCompatActivity
+import android.view.MenuItem
+import com.ted.weather.R
+import com.ted.weather.extensions.DelegatesExt
+import kotlinx.android.synthetic.main.activity_settings.*
+import kotlinx.android.synthetic.main.toolbar.*
+
+class SettingsActivity : AppCompatActivity() {
+
+    companion object {
+        val ZIP_CODE = "zipCode"
+        val DEFAULT_ZIP = 94043L
+    }
+
+    var zipCode: Long by DelegatesExt.preference(this, ZIP_CODE, DEFAULT_ZIP)
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_settings)
+        setSupportActionBar(toolbar)
+        supportActionBar?.setDisplayHomeAsUpEnabled(true)
+        cityCode.setText(zipCode.toString())
+    }
+
+    override fun onBackPressed() {
+        super.onBackPressed()
+        zipCode = cityCode.text.toString().toLong()
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+        android.R.id.home -> {
+            onBackPressed()
+            true
+        }
+        else -> false
+    }
+}

+ 49 - 0
app/src/main/java/com/ted/weather/ui/activity/ToolbarManager.kt

@@ -0,0 +1,49 @@
+package com.ted.weather.ui.activity
+
+import android.support.v7.graphics.drawable.DrawerArrowDrawable
+import android.support.v7.widget.RecyclerView
+import android.support.v7.widget.Toolbar
+import com.ted.weather.ui.App
+import com.ted.weather.R
+import com.ted.weather.extensions.ctx
+import com.ted.weather.extensions.slideEnter
+import com.ted.weather.extensions.slideExit
+import org.jetbrains.anko.startActivity
+import org.jetbrains.anko.toast
+
+interface ToolbarManager {
+
+    val toolbar: Toolbar
+
+    var toolbarTitle: String
+        get() = toolbar.title.toString()
+        set(value) {
+            toolbar.title = value
+        }
+
+    fun initToolbar() {
+        toolbar.inflateMenu(R.menu.menu_main)
+        toolbar.setOnMenuItemClickListener {
+            when (it.itemId) {
+                R.id.action_settings -> toolbar.ctx.startActivity<SettingsActivity>()
+                else -> App.instance.toast("Unknown option")
+            }
+            true
+        }
+    }
+
+    fun enableHomeAsUp(up: () -> Unit) {
+        toolbar.navigationIcon = createUpDrawable()
+        toolbar.setNavigationOnClickListener { up() }
+    }
+
+    private fun createUpDrawable() = DrawerArrowDrawable(toolbar.ctx).apply { progress = 1f }
+
+    fun attachToScroll(recyclerView: RecyclerView) {
+        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
+                if (dy > 0) toolbar.slideExit() else toolbar.slideEnter()
+            }
+        })
+    }
+}

+ 42 - 0
app/src/main/java/com/ted/weather/ui/adapter/ForecastListAdapter.kt

@@ -0,0 +1,42 @@
+package com.ted.weather.ui.adapter
+
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.ted.weather.R
+import com.ted.weather.domain.model.Forecast
+import com.ted.weather.domain.model.ForecastList
+import com.ted.weather.extensions.ctx
+import com.ted.weather.extensions.toDateString
+import com.squareup.picasso.Picasso
+import kotlinx.android.synthetic.main.item_forecast.view.*
+
+class ForecastListAdapter(val weekForecast: ForecastList, val itemClick: (Forecast) -> Unit) :
+        RecyclerView.Adapter<ForecastListAdapter.ViewHolder>() {
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val view = LayoutInflater.from(parent.ctx).inflate(R.layout.item_forecast, parent, false)
+        return ViewHolder(view, itemClick)
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        holder.bindForecast(weekForecast[position])
+    }
+
+    override fun getItemCount() = weekForecast.size
+
+    class ViewHolder(view: View, val itemClick: (Forecast) -> Unit) : RecyclerView.ViewHolder(view) {
+
+        fun bindForecast(forecast: Forecast) {
+            with(forecast) {
+                Picasso.with(itemView.ctx).load(iconUrl).into(itemView.icon)
+                itemView.date.text = date.toDateString()
+                itemView.description.text = description
+                itemView.maxTemperature.text = "{$high}º"
+                itemView.minTemperature.text = "{$low}º"
+                itemView.setOnClickListener { itemClick(this) }
+            }
+        }
+    }
+}

+ 63 - 0
app/src/main/res/layout/activity_detail.xml

@@ -0,0 +1,63 @@
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <include layout="@layout/toolbar"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        android:paddingLeft="@dimen/activity_horizontal_margin"
+        android:paddingRight="@dimen/activity_horizontal_margin"
+        android:paddingTop="@dimen/activity_vertical_margin"
+        tools:ignore="UseCompoundDrawables">
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="64dp"
+            android:layout_height="64dp"
+            tools:ignore="ContentDescription"
+            tools:src="@mipmap/ic_launcher"/>
+
+        <TextView
+            android:id="@+id/weatherDescription"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/spacing_xlarge"
+            android:textAppearance="@style/TextAppearance.AppCompat.Display1"
+            tools:text="Few clouds"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <TextView
+            android:id="@+id/maxTemperature"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/spacing_xlarge"
+            android:layout_weight="1"
+            android:gravity="center_horizontal"
+            android:textAppearance="@style/TextAppearance.AppCompat.Display3"
+            tools:text="30º"/>
+
+        <TextView
+            android:id="@+id/minTemperature"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/spacing_xlarge"
+            android:layout_weight="1"
+            android:gravity="center_horizontal"
+            android:textAppearance="@style/TextAppearance.AppCompat.Display3"
+            tools:text="10º"/>
+
+    </LinearLayout>
+
+</LinearLayout>

+ 9 - 28
app/src/main/res/layout/activity_main.xml

@@ -1,34 +1,15 @@
-<?xml version="1.0" encoding="utf-8"?>
-<android.support.design.widget.CoordinatorLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    tools:context="com.ted.weather.MainActivity">
+    android:layout_height="match_parent">
 
-    <android.support.design.widget.AppBarLayout
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/forecastList"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:theme="@style/AppTheme.AppBarOverlay">
+        android:layout_height="match_parent"
+        android:clipToPadding="false"
+        android:paddingTop="?attr/actionBarSize"/>
 
-        <android.support.v7.widget.Toolbar
-            android:id="@+id/toolbar"
-            android:layout_width="match_parent"
-            android:layout_height="?attr/actionBarSize"
-            android:background="?attr/colorPrimary"
-            app:popupTheme="@style/AppTheme.PopupOverlay"/>
+    <include layout="@layout/toolbar"/>
 
-    </android.support.design.widget.AppBarLayout>
-
-    <include layout="@layout/content_main"/>
-
-    <android.support.design.widget.FloatingActionButton
-        android:id="@+id/fab"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="bottom|end"
-        android:layout_margin="@dimen/fab_margin"
-        app:srcCompat="@android:drawable/ic_dialog_email"/>
-
-</android.support.design.widget.CoordinatorLayout>
+</FrameLayout>

+ 31 - 0
app/src/main/res/layout/activity_settings.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <include layout="@layout/toolbar"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginTop="?attr/actionBarSize"
+        android:orientation="vertical"
+        android:padding="@dimen/spacing_xlarge">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/city_zipcode"/>
+
+        <EditText
+            android:id="@+id/cityCode"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="@string/city_zipcode"
+            android:inputType="number"/>
+
+    </LinearLayout>
+
+</FrameLayout>

+ 0 - 21
app/src/main/res/layout/content_main.xml

@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<android.support.constraint.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    app:layout_behavior="@string/appbar_scrolling_view_behavior"
-    tools:context="com.ted.weather.MainActivity"
-    tools:showIn="@layout/activity_main">
-
-    <TextView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="Hello World!"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintLeft_toLeftOf="parent"
-        app:layout_constraintRight_toRightOf="parent"
-        app:layout_constraintTop_toTopOf="parent"/>
-
-</android.support.constraint.ConstraintLayout>

+ 64 - 0
app/src/main/res/layout/item_forecast.xml

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?attr/selectableItemBackground"
+    android:gravity="center_vertical"
+    android:orientation="horizontal"
+    android:padding="@dimen/spacing_xlarge">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        tools:src="@mipmap/ic_launcher"/>
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="@dimen/spacing_xlarge"
+        android:layout_marginRight="@dimen/spacing_xlarge"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/date"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+            tools:text="May 14, 2015"/>
+
+        <TextView
+            android:id="@+id/description"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textAppearance="@style/TextAppearance.AppCompat.Caption"
+            tools:text="Light Rain"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/maxTemperature"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+            tools:text="30º"/>
+
+        <TextView
+            android:id="@+id/minTemperature"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="@style/TextAppearance.AppCompat.Caption"
+            tools:text="15º"/>
+
+    </LinearLayout>
+
+</LinearLayout>

+ 11 - 0
app/src/main/res/layout/toolbar.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<android.support.v7.widget.Toolbar
+    android:id="@+id/toolbar"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="?attr/actionBarSize"
+    android:background="?attr/colorPrimary"
+    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

+ 1 - 1
app/src/main/res/menu/menu_main.xml

@@ -1,7 +1,7 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res-auto"
       xmlns:tools="http://schemas.android.com/tools"
-      tools:context="com.ted.weather.MainActivity">
+      tools:context="com.ted.weather.ui.activity.MainActivity">
     <item
         android:id="@+id/action_settings"
         android:orderInCategory="100"

+ 5 - 0
app/src/main/res/values/dimens.xml

@@ -1,3 +1,8 @@
 <resources>
     <dimen name="fab_margin">16dp</dimen>
+    <dimen name="spacing_xlarge">16dp</dimen>
+
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
 </resources>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -1,4 +1,6 @@
 <resources>
     <string name="app_name">WeatherApp_Kotlin</string>
     <string name="action_settings">Settings</string>
+    <string name="city_zipcode">City ZipCode</string>
+    <string name="setting">Setting</string>
 </resources>

+ 6 - 8
build.gradle

@@ -1,22 +1,20 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
+apply from:'version.gradle'
 
 buildscript {
+    ext.kotlin_version = '1.1.3-2'
+    ext.anko_version = '0.10.1'
+
     repositories {
         jcenter()
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:2.3.3'
-
-        // NOTE: Do not place your application dependencies here; they belong
-        // in the individual module build.gradle files
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+        classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
     }
 }
 
-allprojects {
-    repositories {
-        jcenter()
-    }
-}
 
 task clean(type: Delete) {
     delete rootProject.buildDir

+ 3 - 1
version.gradle

@@ -9,6 +9,7 @@ ext {
     minSdkVersion = 16
     targetSdkVersion = 26
     appcompatV7 = "com.android.support:appcompat-v7:$androidSupportVersion"
+    recycleView = "com.android.support:recyclerview-v7:$androidSupportVersion"
     design = "com.android.support:design:$androidSupportVersion"
     constraintLayout = "com.android.support.constraint:constraint-layout:$androidConstraintLayout"
 }
@@ -20,4 +21,5 @@ allprojects {
             url "https://maven.google.com"
         }
     }
-}
+}
+