From 36a1a00f870848d914fa186572b7ce5db833528c Mon Sep 17 00:00:00 2001 From: Anel CC Date: Wed, 15 Apr 2020 00:04:27 -0400 Subject: [PATCH 1/7] [Anel] Acquire data in a repository class --- .../java/com/anelcc/monster/MainViewModel.kt | 21 ++-------------- .../anelcc/monster/data/MonsterRepository.kt | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt diff --git a/app/src/main/java/com/anelcc/monster/MainViewModel.kt b/app/src/main/java/com/anelcc/monster/MainViewModel.kt index 740be75..45eee65 100644 --- a/app/src/main/java/com/anelcc/monster/MainViewModel.kt +++ b/app/src/main/java/com/anelcc/monster/MainViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import android.util.Log import androidx.lifecycle.AndroidViewModel import com.anelcc.monster.data.Monster +import com.anelcc.monster.data.MonsterRepository import com.anelcc.monster.utilities.FileHelper import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi @@ -12,26 +13,8 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory class MainViewModel(app: Application) : AndroidViewModel(app) { - private val listType = Types.newParameterizedType( - List::class.java, Monster::class.java - ) - init { - val resourcesText = FileHelper.getTextFromResources(app, R.raw.monster_data) - Log.i(LOG_TAG, "Resources: $resourcesText") - - val assetsText = FileHelper.getTextFromAssets(app, "monster_data.json") - Log.i(LOG_TAG, "Assets: $assetsText") - - parseText(assetsText) - } - - fun parseText(text: String) { - val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() - val adapter: JsonAdapter> = moshi.adapter(listType) - //This is parsing the data - val monsterData = adapter.fromJson(text) - + val monsterData = MonsterRepository().getMonsterData(app) for (monster in monsterData ?: emptyList()) { Log.i(LOG_TAG,"parseText: ${monster.name} (\$${monster.price})") } diff --git a/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt b/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt new file mode 100644 index 0000000..b8263f5 --- /dev/null +++ b/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt @@ -0,0 +1,24 @@ +package com.anelcc.monster.data + +import android.content.Context +import android.util.Log +import com.anelcc.monster.LOG_TAG +import com.anelcc.monster.utilities.FileHelper +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory + +class MonsterRepository { + + private val listType = Types.newParameterizedType( + List::class.java, Monster::class.java + ) + + fun getMonsterData(context: Context): List { + val assetsText = FileHelper.getTextFromAssets(context, "monster_data.json" ) + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + val adapter: JsonAdapter> = moshi.adapter(listType) + return adapter.fromJson(assetsText) ?: emptyList() + } +} \ No newline at end of file From 548111cc87ba1d4cc616ffc9ab5c0d0d96d14651 Mon Sep 17 00:00:00 2001 From: Anel CC Date: Wed, 15 Apr 2020 00:04:27 -0400 Subject: [PATCH 2/7] [Anel] Acquire data in a repository class --- app/src/main/java/com/anelcc/monster/MainViewModel.kt | 8 +------- .../java/com/anelcc/monster/data/MonsterRepository.kt | 2 -- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/app/src/main/java/com/anelcc/monster/MainViewModel.kt b/app/src/main/java/com/anelcc/monster/MainViewModel.kt index 45eee65..fa225ef 100644 --- a/app/src/main/java/com/anelcc/monster/MainViewModel.kt +++ b/app/src/main/java/com/anelcc/monster/MainViewModel.kt @@ -3,20 +3,14 @@ package com.anelcc.monster import android.app.Application import android.util.Log import androidx.lifecycle.AndroidViewModel -import com.anelcc.monster.data.Monster import com.anelcc.monster.data.MonsterRepository -import com.anelcc.monster.utilities.FileHelper -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory class MainViewModel(app: Application) : AndroidViewModel(app) { init { val monsterData = MonsterRepository().getMonsterData(app) for (monster in monsterData ?: emptyList()) { - Log.i(LOG_TAG,"parseText: ${monster.name} (\$${monster.price})") + Log.i(LOG_TAG, "parseText: ${monster.name} (\$${monster.price})") } } } diff --git a/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt b/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt index b8263f5..5ec3e48 100644 --- a/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt +++ b/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt @@ -1,8 +1,6 @@ package com.anelcc.monster.data import android.content.Context -import android.util.Log -import com.anelcc.monster.LOG_TAG import com.anelcc.monster.utilities.FileHelper import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi From e16d099daf55eb09d458275a20812252de4ff78a Mon Sep 17 00:00:00 2001 From: Anel CC Date: Wed, 15 Apr 2020 00:41:22 -0400 Subject: [PATCH 3/7] [Anel] Share data with LiveData objects --- .../java/com/anelcc/monster/MainViewModel.kt | 14 +++++++------- .../anelcc/monster/data/MonsterRepository.kt | 19 ++++++++++++++----- .../com/anelcc/monster/main/MainFragment.kt | 18 +++++++++++++++--- app/src/main/res/layout/fragment_main.xml | 1 + 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/anelcc/monster/MainViewModel.kt b/app/src/main/java/com/anelcc/monster/MainViewModel.kt index fa225ef..d0abe28 100644 --- a/app/src/main/java/com/anelcc/monster/MainViewModel.kt +++ b/app/src/main/java/com/anelcc/monster/MainViewModel.kt @@ -1,16 +1,16 @@ package com.anelcc.monster import android.app.Application -import android.util.Log import androidx.lifecycle.AndroidViewModel import com.anelcc.monster.data.MonsterRepository class MainViewModel(app: Application) : AndroidViewModel(app) { - init { - val monsterData = MonsterRepository().getMonsterData(app) - for (monster in monsterData ?: emptyList()) { - Log.i(LOG_TAG, "parseText: ${monster.name} (\$${monster.price})") - } - } + /* + The ViewModel is simply passing that LiveData object back to the user interface. + And the fragment, that is the user interface, + is only responsible for managing the presentation. + */ + private val dataRepo = MonsterRepository(app) + val monsterData = dataRepo.monsterData } diff --git a/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt b/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt index 5ec3e48..fd01b24 100644 --- a/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt +++ b/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt @@ -1,22 +1,31 @@ package com.anelcc.monster.data -import android.content.Context +import android.app.Application +import androidx.lifecycle.MutableLiveData import com.anelcc.monster.utilities.FileHelper import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -class MonsterRepository { +//I want an application reference. +class MonsterRepository(val app: Application) { + + //instance of the class MutableLiveData + val monsterData = MutableLiveData>() private val listType = Types.newParameterizedType( List::class.java, Monster::class.java ) - fun getMonsterData(context: Context): List { - val assetsText = FileHelper.getTextFromAssets(context, "monster_data.json" ) + init { + getMonsterData() + } + + fun getMonsterData() { + val assetsText = FileHelper.getTextFromAssets(app, "monster_data.json" ) val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val adapter: JsonAdapter> = moshi.adapter(listType) - return adapter.fromJson(assetsText) ?: emptyList() + monsterData.value = adapter.fromJson(assetsText) ?: emptyList() } } \ No newline at end of file diff --git a/app/src/main/java/com/anelcc/monster/main/MainFragment.kt b/app/src/main/java/com/anelcc/monster/main/MainFragment.kt index 2505cf8..bddda44 100644 --- a/app/src/main/java/com/anelcc/monster/main/MainFragment.kt +++ b/app/src/main/java/com/anelcc/monster/main/MainFragment.kt @@ -7,11 +7,13 @@ import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import com.anelcc.monster.LOG_TAG import com.anelcc.monster.MainViewModel import com.anelcc.monster.R -import com.anelcc.monster.data.Monster +import kotlinx.android.synthetic.main.fragment_main.* class MainFragment : Fragment() { @@ -25,9 +27,19 @@ class MainFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + /*That will notify the ViewModel when changes happen. And then I need an Observer object. + Within the Observer object, I'll receive the data as a list of Monster objects. So I'll go back to my ViewModel.*/ + viewModel = ViewModelProvider(this).get(MainViewModel::class.java) - val monster = Monster("Bob", "myfile", "a caption", "a description", .19, 3) - Log.i("monsterLogging", monster.toString()) + viewModel.monsterData.observe(this, Observer { + val monsterNames = StringBuilder() + /*I'll reference it as it. That is, this is the value that was passed in when the observer class received changes to the data*/ + for (monster in it) { + monsterNames.append(monster.name).append("\n") + //Log.i(LOG_TAG, "parseText: ${monster.name} (\$${monster.price})") + } + monster_name.text = monsterNames + }) return inflater.inflate(R.layout.fragment_main, container, false) } diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 95e9363..4b8a22d 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -7,6 +7,7 @@ From b36581a6535cf69a40b4960713e2af3ec9d0088c Mon Sep 17 00:00:00 2001 From: Anel CC Date: Wed, 15 Apr 2020 01:12:53 -0400 Subject: [PATCH 4/7] [Anel] Network permissions and status --- app/src/main/AndroidManifest.xml | 3 +++ .../com/anelcc/monster/data/MonsterRepository.kt | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d151c55..23f274f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + > = moshi.adapter(listType) monsterData.value = adapter.fromJson(assetsText) ?: emptyList() } + + @Suppress("DEPRECATION") + private fun networkAvailable(): Boolean { + val connectivityManager = app.getSystemService(Context.CONNECTIVITY_SERVICE) + as ConnectivityManager + val networkInfo = connectivityManager.activeNetworkInfo + return networkInfo?.isConnectedOrConnecting ?: false + } } \ No newline at end of file From 3c80328f8a23e60e1aa9a1f23814da773f26863f Mon Sep 17 00:00:00 2001 From: Anel CC Date: Wed, 15 Apr 2020 01:20:11 -0400 Subject: [PATCH 5/7] [Anel] Define a Retrofit interface --- .idea/misc.xml | 2 +- app/build.gradle | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 7bfef59..37a7509 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/app/build.gradle b/app/build.gradle index f1048ce..47f8ee6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,11 +43,22 @@ dependencies { // KTX implementation 'androidx.core:core-ktx:1.0.1' implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0" // This container view models and data implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + //add the retrofit2 dependencies + implementation 'com.squareup.retrofit2:converter-gson:2.6.0' + implementation 'com.squareup.retrofit2:retrofit:2.6.0' // This container view models and data implementation 'com.squareup.moshi:moshi-kotlin:1.9.2' kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.9.2' + + //Coroutines: Web service calls in Android have to be made asynchronously and they have to be done in background threads. + // Web service calls in Android are asynchronous that is you have to set up the call + // and then respond in some fashion when the response comes back. + // The web request and response must be handled in background threads. + // In the past, Android developers had a variety of ways to work with background threads, + // but there's a new way using coroutines a feature of the kotlin language. } From 4ff060f3608653abd812e14a0e785eed57e1cc43 Mon Sep 17 00:00:00 2001 From: Anel CC Date: Wed, 15 Apr 2020 01:33:49 -0400 Subject: [PATCH 6/7] [Anel] Define a Retrofit interface --- app/src/main/java/com/anelcc/monster/Global.kt | 3 ++- .../java/com/anelcc/monster/data/MonsterService.kt | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/anelcc/monster/data/MonsterService.kt diff --git a/app/src/main/java/com/anelcc/monster/Global.kt b/app/src/main/java/com/anelcc/monster/Global.kt index f00d15b..6620400 100644 --- a/app/src/main/java/com/anelcc/monster/Global.kt +++ b/app/src/main/java/com/anelcc/monster/Global.kt @@ -1,3 +1,4 @@ package com.anelcc.monster -const val LOG_TAG = "monsterLogging" \ No newline at end of file +const val LOG_TAG = "monsterLogging" +const val WEB_SERVICE_URL = "https://774906.youcanlearnit.net" \ No newline at end of file diff --git a/app/src/main/java/com/anelcc/monster/data/MonsterService.kt b/app/src/main/java/com/anelcc/monster/data/MonsterService.kt new file mode 100644 index 0000000..613997a --- /dev/null +++ b/app/src/main/java/com/anelcc/monster/data/MonsterService.kt @@ -0,0 +1,14 @@ +package com.anelcc.monster.data + +import retrofit2.Response +import retrofit2.http.GET + +interface MonsterService { + //Add the keyword suspend. + // That means that this function is designed to be called from with a coroutine. + // Coroutines can run either on the UI thread or in a background thread, + // but for a function to be part of a coroutine call, + // it must have the suspend keyword at the beginning of the function declaration. + @GET("/feed/monster_data.json") + suspend fun getMonsterData(): Response> +} \ No newline at end of file From c3ea81d1aa5a536dffac1240eda3f18c7df8bbe7 Mon Sep 17 00:00:00 2001 From: Anel CC Date: Wed, 15 Apr 2020 01:58:00 -0400 Subject: [PATCH 7/7] [Anel] Retrieve remote data with Retrofit --- app/build.gradle | 2 + .../main/java/com/anelcc/monster/Global.kt | 2 +- .../java/com/anelcc/monster/data/Monster.kt | 4 +- .../anelcc/monster/data/MonsterRepository.kt | 45 +++++++++++++------ .../com/anelcc/monster/data/MonsterService.kt | 2 +- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 47f8ee6..5787635 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,8 +50,10 @@ dependencies { //add the retrofit2 dependencies implementation 'com.squareup.retrofit2:converter-gson:2.6.0' + implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' implementation 'com.squareup.retrofit2:retrofit:2.6.0' // This container view models and data + implementation 'com.squareup.moshi:moshi-kotlin:1.9.2' kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.9.2' diff --git a/app/src/main/java/com/anelcc/monster/Global.kt b/app/src/main/java/com/anelcc/monster/Global.kt index 6620400..18c5242 100644 --- a/app/src/main/java/com/anelcc/monster/Global.kt +++ b/app/src/main/java/com/anelcc/monster/Global.kt @@ -1,4 +1,4 @@ package com.anelcc.monster const val LOG_TAG = "monsterLogging" -const val WEB_SERVICE_URL = "https://774906.youcanlearnit.net" \ No newline at end of file +const val WEB_SERVICE_URL = "https://774906.youcanlearnit.net/" \ No newline at end of file diff --git a/app/src/main/java/com/anelcc/monster/data/Monster.kt b/app/src/main/java/com/anelcc/monster/data/Monster.kt index 727a983..1cfd5ee 100644 --- a/app/src/main/java/com/anelcc/monster/data/Monster.kt +++ b/app/src/main/java/com/anelcc/monster/data/Monster.kt @@ -1,9 +1,9 @@ package com.anelcc.monster.data -import com.squareup.moshi.Json +import com.google.gson.annotations.SerializedName data class Monster ( - @Json(name = "monsterName") + @SerializedName("monsterName") val name: String, val imageFile: String, val caption: String, diff --git a/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt b/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt index 22eb863..0d7752b 100644 --- a/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt +++ b/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt @@ -4,35 +4,52 @@ import android.app.Application import android.content.Context import android.net.ConnectivityManager import android.util.Log +import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import com.anelcc.monster.LOG_TAG -import com.anelcc.monster.utilities.FileHelper -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi +import com.anelcc.monster.WEB_SERVICE_URL +import com.google.gson.GsonBuilder import com.squareup.moshi.Types -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.moshi.MoshiConverterFactory -//I want an application reference. class MonsterRepository(val app: Application) { //instance of the class MutableLiveData val monsterData = MutableLiveData>() - private val listType = Types.newParameterizedType( List::class.java, Monster::class.java ) init { - getMonsterData() - Log.i(LOG_TAG, "Anel Network available: ${networkAvailable()}") - + //Co-routine scope pass in a dispatcher. + // There are a number of different dispatchers available in the co-routines library, + // but for Android you can typically choose between two. + // Dispatchers.io means do this on the background thread, + // while dispatchers.main means do it in the foreground thread. + CoroutineScope(Dispatchers.IO).launch { + callWebService() + } } - fun getMonsterData() { - val assetsText = FileHelper.getTextFromAssets(app, "monster_data.json" ) - val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() - val adapter: JsonAdapter> = moshi.adapter(listType) - monsterData.value = adapter.fromJson(assetsText) ?: emptyList() + /** + * WorkerThreat annotation. Is an indicator that this function will be called in a background threat. + */ + @WorkerThread + suspend fun callWebService() { + if (networkAvailable()) { + val retrofit = Retrofit.Builder() + .baseUrl(WEB_SERVICE_URL) + .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create())) + .build() + val service = retrofit.create(MonsterService::class.java) + val serviceData = service.getMonsterData().body() ?: emptyList() + monsterData.postValue(serviceData) + } } @Suppress("DEPRECATION") diff --git a/app/src/main/java/com/anelcc/monster/data/MonsterService.kt b/app/src/main/java/com/anelcc/monster/data/MonsterService.kt index 613997a..86c0b4b 100644 --- a/app/src/main/java/com/anelcc/monster/data/MonsterService.kt +++ b/app/src/main/java/com/anelcc/monster/data/MonsterService.kt @@ -9,6 +9,6 @@ interface MonsterService { // Coroutines can run either on the UI thread or in a background thread, // but for a function to be part of a coroutine call, // it must have the suspend keyword at the beginning of the function declaration. - @GET("/feed/monster_data.json") + @GET("feed/monster_data.json") suspend fun getMonsterData(): Response> } \ No newline at end of file