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..5787635 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,11 +43,24 @@ 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: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' + + //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. } 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) - //This is parsing the data - val monsterData = adapter.fromJson(text) - - 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/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 new file mode 100644 index 0000000..0d7752b --- /dev/null +++ b/app/src/main/java/com/anelcc/monster/data/MonsterRepository.kt @@ -0,0 +1,62 @@ +package com.anelcc.monster.data + +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.WEB_SERVICE_URL +import com.google.gson.GsonBuilder +import com.squareup.moshi.Types +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 + +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 { + //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() + } + } + + /** + * 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") + 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 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..86c0b4b --- /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 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 @@