From de81e4d876cc26d360017f7524a66491d7b6e3de Mon Sep 17 00:00:00 2001 From: Yu Xin Date: Thu, 17 Nov 2022 12:12:54 +0000 Subject: [PATCH 1/5] 11/17 milestone2 --- app/build.gradle | 19 ++- app/src/main/AndroidManifest.xml | 6 +- .../com/example/assignment/BindingAdapters.kt | 15 ++ .../assignment/adapter/MeteorsListAdapter.kt | 144 ++++++----------- .../example/assignment/model/MeteorData.kt | 17 +- .../assignment/network/MeteorApiService.kt | 49 ++++++ .../com/example/assignment/ui/MainActivity.kt | 61 -------- .../com/example/assignment/ui/MapActivity.kt | 59 ------- .../example/assignment/view/MainActivity.kt | 64 ++++++++ .../example/assignment/view/MapActivity.kt | 58 +++++++ .../assignment/viewModel/MainViewModel.kt | 53 +++++++ app/src/main/res/layout/activity_main.xml | 44 ++++-- app/src/main/res/layout/activity_map.xml | 2 +- app/src/main/res/layout/item_meteorite.xml | 147 ++++++++++-------- 14 files changed, 417 insertions(+), 321 deletions(-) create mode 100644 app/src/main/java/com/example/assignment/BindingAdapters.kt create mode 100644 app/src/main/java/com/example/assignment/network/MeteorApiService.kt delete mode 100644 app/src/main/java/com/example/assignment/ui/MainActivity.kt delete mode 100644 app/src/main/java/com/example/assignment/ui/MapActivity.kt create mode 100644 app/src/main/java/com/example/assignment/view/MainActivity.kt create mode 100644 app/src/main/java/com/example/assignment/view/MapActivity.kt create mode 100644 app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index 16d2f0b..eb330fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' } android { @@ -16,16 +17,13 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - buildFeatures { - viewBinding = true - } - buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -41,21 +39,32 @@ android { } } buildFeatures { - viewBinding true + dataBinding true } + } dependencies { implementation 'com.google.code.gson:gson:2.8.5' implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'com.google.android.material:material:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2' implementation 'androidx.navigation:navigation-ui-ktx:2.5.2' + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' + implementation 'com.google.android.gms:play-services-basement:17.3.0' implementation 'com.google.android.gms:play-services-maps:17.0.0' + // Retrofit with Moshi Converter + implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' + // Moshi + implementation 'com.squareup.moshi:moshi-kotlin:1.13.0' + // Coil + implementation "io.coil-kt:coil:2.2.2" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a9f6be1..01d4757 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" package="com.example.assignment"> + + - @@ -26,7 +28,7 @@ diff --git a/app/src/main/java/com/example/assignment/BindingAdapters.kt b/app/src/main/java/com/example/assignment/BindingAdapters.kt new file mode 100644 index 0000000..80136f1 --- /dev/null +++ b/app/src/main/java/com/example/assignment/BindingAdapters.kt @@ -0,0 +1,15 @@ +package com.example.assignment + +import androidx.databinding.BindingAdapter +import androidx.recyclerview.widget.RecyclerView +import com.example.assignment.adapter.MeteorsListAdapter +import com.example.assignment.model.MeteorData + +/** + * Updates the data shown in the [RecyclerView]. + */ +@BindingAdapter("listData") +fun bindRecyclerView(recyclerView: RecyclerView, data: List?) { + val adapter = recyclerView.adapter as MeteorsListAdapter + adapter.submitList(data) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt b/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt index b100220..a3cb02b 100644 --- a/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt +++ b/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt @@ -1,124 +1,68 @@ package com.example.assignment.adapter -import android.content.Context -import android.util.Log import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import com.example.assignment.R import com.example.assignment.databinding.ItemMeteoriteBinding import com.example.assignment.model.MeteorData -import com.google.gson.Gson -import java.io.IOException -import java.io.InputStream -class MeteorsListAdapter( - context: Context, - recyclerItemClickListener: RecyclerItemClickListener -) : RecyclerView.Adapter() { +/** + * This class implements a [RecyclerView] [ListAdapter] which uses Data Binding to present [List] + * data, including computing diffs between lists. + */ +class MeteorsListAdapter() : ListAdapter(DiffCallback) { private val TAG: String = this.javaClass.name - var meteorDataList: MutableList - var context: Context - var recyclerItemClickListener: RecyclerItemClickListener - init { - this.context = context - meteorDataList = parseJsonToObject(context) as MutableList - Log.d(TAG, meteorDataList.toString()) - this.recyclerItemClickListener = recyclerItemClickListener - } - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): MeteorsViewHolder { - Log.d(TAG, "onCreateViewHolder called!!") - val li = LayoutInflater.from(context) - return MeteorsViewHolder(ItemMeteoriteBinding.inflate(li)) - } - - override fun onBindViewHolder( - holder: MeteorsViewHolder, - position: Int - ) { - Log.d(TAG, "onBindViewHolder bind position:$position") - val meteorDataData: MeteorData = meteorDataList[position] - holder.bind(meteorDataData) - } - - override fun getItemCount(): Int { - Log.d(TAG, "MeteorsListAdapter getItemCount=" + meteorDataList.size) - return meteorDataList.size - } - - private fun remove(item: MeteorData) { - Log.d(TAG, "remove list item:$item") - val pos = meteorDataList.indexOf(item) - if (pos > -1) { - meteorDataList.removeAt(pos) - notifyItemRemoved(pos) + /** + * The MarsPhotosViewHolder constructor takes the binding variable from the associated + * GridViewItem, which nicely gives it access to the full [MeteorData] information. + */ + class MeteorsViewHolder( + private var binding: ItemMeteoriteBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(meteorData: MeteorData) { + binding.meteor = meteorData + // This is important, because it forces the data binding to execute immediately, + // which allows the RecyclerView to make the correct view size measurements + binding.executePendingBindings() } } - private fun getItem(position: Int): MeteorData { - return meteorDataList[position] - } - - fun clear() { - Log.d(TAG, "clear Recycler view.") - while (itemCount > 0) { - remove(getItem(0)) + /** + * Allows the RecyclerView to determine which items have changed when the [List] of + * [MeteorData] has been updated. + */ + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MeteorData, newItem: MeteorData): Boolean { + return oldItem.id == newItem.id } - } - - interface RecyclerItemClickListener { - fun onRecyclerItemClick(meteorItem: MeteorData?) - } - inner class MeteorsViewHolder(itemBinding: ItemMeteoriteBinding) : - RecyclerView.ViewHolder(itemBinding.getRoot()) { - var itemMeteoriteBinding: ItemMeteoriteBinding - fun bind(meteorDataData: MeteorData) { - Log.d(TAG, "MeteorsViewHolder bind started") - itemMeteoriteBinding.name.setText(meteorDataData.name) - itemMeteoriteBinding.mass.setText(context.getString(R.string.main_mass, meteorDataData.mass)) - itemMeteoriteBinding.location.setText(meteorDataData.reclat.toString() + ", " + meteorDataData.reclong.toString()) - itemMeteoriteBinding.date.setText(meteorDataData.year?.substring(0, meteorDataData.year!!.indexOf("-"))) - } - - init { - itemMeteoriteBinding = itemBinding - itemBinding.getRoot().setOnClickListener(View.OnClickListener { - Log.d(TAG,"MeteorsViewHolder clicked pos=" + meteorDataList[adapterPosition]) - recyclerItemClickListener.onRecyclerItemClick(meteorDataList[adapterPosition]) - }) + override fun areContentsTheSame(oldItem: MeteorData, newItem: MeteorData): Boolean { + return oldItem.name == newItem.name } } - private fun loadJSONFromAsset(context: Context): String? { - var json: String? = null - json = try { - val `is`: InputStream = context.assets.open("y77d-th95.json") - val size: Int = `is`.available() - val buffer = ByteArray(size) - `is`.read(buffer) - `is`.close() - String(buffer) - } catch (ex: IOException) { - ex.printStackTrace() - return null - } - return json + /** + * Create new [RecyclerView] item views (invoked by the layout manager) + */ + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): MeteorsViewHolder { + return MeteorsViewHolder( + ItemMeteoriteBinding.inflate(LayoutInflater.from(parent.context)) + ) } - private fun parseJsonToObject(context: Context): List { - val jsonString: String? = loadJSONFromAsset(context) - return Gson().fromJson(jsonString, Array::class.java).toList() + /** + * Replaces the contents of a view (invoked by the layout manager) + */ + override fun onBindViewHolder(holder: MeteorsViewHolder, position: Int) { + val marsPhoto = getItem(position) + holder.bind(marsPhoto) } - - - } \ No newline at end of file diff --git a/app/src/main/java/com/example/assignment/model/MeteorData.kt b/app/src/main/java/com/example/assignment/model/MeteorData.kt index a4ff49c..2d6401e 100644 --- a/app/src/main/java/com/example/assignment/model/MeteorData.kt +++ b/app/src/main/java/com/example/assignment/model/MeteorData.kt @@ -1,12 +1,13 @@ package com.example.assignment.model +import com.squareup.moshi.Json -open class MeteorData(){ - var id: String? = null - var mass = 0.0 - var name: String? = null - var reclat: String? = null - var reclong: String? = null - var year: String? = null -} \ No newline at end of file +data class MeteorData( + @Json(name = "id") var id: String? = null, + @Json(name = "mass") var mass: Double = 0.0, + @Json(name = "name") var name: String? = null, + @Json(name = "reclat") var reclat: String? = null, + @Json(name = "reclong") var reclong: String? = null, + @Json(name = "year") var year: String? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/example/assignment/network/MeteorApiService.kt b/app/src/main/java/com/example/assignment/network/MeteorApiService.kt new file mode 100644 index 0000000..cf0e8e2 --- /dev/null +++ b/app/src/main/java/com/example/assignment/network/MeteorApiService.kt @@ -0,0 +1,49 @@ +package com.example.assignment.network + +import com.example.assignment.model.MeteorData +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.http.GET + +private const val BASE_URL = "https://data.nasa.gov/" + +/** + * Build the Moshi object that Retrofit will be using, making sure to add the Kotlin adapter for + * full Kotlin compatibility. + */ +private val moshi = Moshi.Builder() + .add(KotlinJsonAdapterFactory()) + .build() + +/** + * Use the Retrofit builder to build a com.example.assignment.network.retrofit object using a Moshi converter with our Moshi + * object. + */ +private val retrofit = Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .baseUrl(BASE_URL) + .build() + +/** + * A public interface that exposes the [getMeteorsInfo] method + */ +interface MeteorsApiService { + /** + * Returns a [List] of [MeteorData] and this method can be called from a Coroutine. + * The @GET annotation indicates that the "photos" endpoint will be requested with the GET + * HTTP method + */ + @GET("resource/y77d-th95.json?\$where=year>='1900-01-01T00:00:00.000'")//&\$limit=15") + suspend fun getMeteorsInfo(): List +} + +/** + * A public Api object that exposes the lazy-initialized Retrofit service + */ +object MeteorsApi { + val retrofitService: MeteorsApiService by lazy { + retrofit.create(MeteorsApiService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/assignment/ui/MainActivity.kt b/app/src/main/java/com/example/assignment/ui/MainActivity.kt deleted file mode 100644 index 4b8cd61..0000000 --- a/app/src/main/java/com/example/assignment/ui/MainActivity.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.assignment.ui - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.util.Log -import android.view.Menu -import android.view.MenuItem -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.example.assignment.databinding.ActivityMainBinding -import com.example.assignment.R -import com.example.assignment.adapter.MeteorsListAdapter -import com.example.assignment.model.MeteorData -import com.example.assignment.util.Constant - -class MainActivity : AppCompatActivity(), MeteorsListAdapter.RecyclerItemClickListener { - private val TAG: String = this.javaClass.name - lateinit var meteorsListAdapter: MeteorsListAdapter - lateinit var activityMainBinding: ActivityMainBinding - lateinit var context: Context - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - this.context = getApplicationContext() - - //get layout view - activityMainBinding = ActivityMainBinding.inflate(getLayoutInflater()) - val view: View = activityMainBinding.getRoot() - setContentView(view) - - val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this) - activityMainBinding.recycler.setLayoutManager(layoutManager) - - fillRecyclerViewData() - - } - - override fun onCreateOptionsMenu(menu: Menu?): Boolean { - getMenuInflater().inflate(R.menu.menu, menu) - return true - } - - - fun fillRecyclerViewData() { - Log.d(TAG, "fillRecyclerViewData called!!") - meteorsListAdapter = MeteorsListAdapter(this,this) - activityMainBinding.recycler.setAdapter(meteorsListAdapter) - } - - override fun onRecyclerItemClick(meteorItem: MeteorData?) { - Log.d(TAG, "onRecyclerItemClick called!!") - val mapActivityIntent = Intent(this, MapActivity::class.java) - mapActivityIntent.putExtra(Constant.Intent.METEORITE_NAME, meteorItem?.name) - mapActivityIntent.putExtra(Constant.Intent.METEORITE_LAT, meteorItem?.reclat) - mapActivityIntent.putExtra(Constant.Intent.METEORITE_LNG, meteorItem?.reclong) - startActivity(mapActivityIntent) - } -} diff --git a/app/src/main/java/com/example/assignment/ui/MapActivity.kt b/app/src/main/java/com/example/assignment/ui/MapActivity.kt deleted file mode 100644 index de540f4..0000000 --- a/app/src/main/java/com/example/assignment/ui/MapActivity.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.example.assignment.ui - -import android.os.Bundle -import android.util.Log -import android.view.MenuItem -import androidx.appcompat.app.ActionBar -import androidx.appcompat.app.AppCompatActivity -import com.example.assignment.R -import com.example.assignment.databinding.ActivityMapBinding -import com.example.assignment.util.Constant -import com.google.android.gms.maps.CameraUpdateFactory -import com.google.android.gms.maps.GoogleMap -import com.google.android.gms.maps.OnMapReadyCallback -import com.google.android.gms.maps.SupportMapFragment -import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.MarkerOptions - - -class MapActivity : AppCompatActivity(), OnMapReadyCallback { - private var TAG: String? = null - private var meteorName: String? = null - private var lat = 0.0 - private var lng = 0.0 - var actionBar: ActionBar? = null - var activityMapBinding: ActivityMapBinding? = null - - override fun onCreate(savedInstanceState: Bundle?) { - TAG = this.javaClass.name - Log.d(TAG, "onCreate called") - super.onCreate(savedInstanceState) - activityMapBinding = ActivityMapBinding.inflate(getLayoutInflater()) - setContentView(activityMapBinding!!.getRoot()) - meteorName = intent.getStringExtra(Constant.Intent.METEORITE_NAME) - lat = intent.getStringExtra(Constant.Intent.METEORITE_LAT)!!.toDouble() - lng = intent.getStringExtra(Constant.Intent.METEORITE_LNG)!!.toDouble() - val supportMapFragment = - supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment? - supportMapFragment!!.getMapAsync(this) - actionBar = supportActionBar - actionBar!!.setDisplayHomeAsUpEnabled(true) - actionBar!!.setTitle(meteorName) - } - - override fun onMapReady(googleMap: GoogleMap) { - googleMap.mapType = GoogleMap.MAP_TYPE_HYBRID - val latLng = LatLng(lat, lng) - googleMap.addMarker( - MarkerOptions() - .position(latLng) - .title(meteorName) - ) - googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, Constant.Map.CAMERA_ZOOM)) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - onBackPressed() - return true - } -} diff --git a/app/src/main/java/com/example/assignment/view/MainActivity.kt b/app/src/main/java/com/example/assignment/view/MainActivity.kt new file mode 100644 index 0000000..240edab --- /dev/null +++ b/app/src/main/java/com/example/assignment/view/MainActivity.kt @@ -0,0 +1,64 @@ +package com.example.assignment.view + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.View +import android.widget.AdapterView +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.example.assignment.databinding.ActivityMainBinding +import com.example.assignment.R +import com.example.assignment.adapter.MeteorsListAdapter +import com.example.assignment.model.MeteorData +import com.example.assignment.util.Constant +import com.example.assignment.viewModel.MainViewModel + +class MainActivity : AppCompatActivity(){ + private val TAG: String = this.javaClass.name + private val viewModel: MainViewModel by viewModels() + lateinit var meteorsListAdapter: MeteorsListAdapter + lateinit var context: Context + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + this.context = getApplicationContext() + + //get layout view + val binding =ActivityMainBinding.inflate(getLayoutInflater()) + + val view: View = binding.getRoot() + setContentView(view) + + // Allows Data Binding to Observe LiveData with the lifecycle of this Fragment + binding.lifecycleOwner = this + + // Giving the binding access to the OverviewViewModel + binding.viewModel = viewModel + + val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this) + binding.recycler.setLayoutManager(layoutManager) + + // Sets the adapter of the photosGrid RecyclerView + meteorsListAdapter = MeteorsListAdapter() + binding.recycler.setAdapter(meteorsListAdapter) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + getMenuInflater().inflate(R.menu.menu, menu) + return true + } + +// override fun onRecyclerItemClick(meteorItem: MeteorData?) { +// Log.d(TAG, "onRecyclerItemClick called!!") +// val mapActivityIntent = Intent(this, MapActivity::class.java) +// mapActivityIntent.putExtra(Constant.Intent.METEORITE_NAME, meteorItem?.name) +// mapActivityIntent.putExtra(Constant.Intent.METEORITE_LAT, meteorItem?.reclat) +// mapActivityIntent.putExtra(Constant.Intent.METEORITE_LNG, meteorItem?.reclong) +// startActivity(mapActivityIntent) +// } +} diff --git a/app/src/main/java/com/example/assignment/view/MapActivity.kt b/app/src/main/java/com/example/assignment/view/MapActivity.kt new file mode 100644 index 0000000..d7be97f --- /dev/null +++ b/app/src/main/java/com/example/assignment/view/MapActivity.kt @@ -0,0 +1,58 @@ +package com.example.assignment.view + +import android.os.Bundle +import android.util.Log +import android.view.MenuItem +import androidx.appcompat.app.ActionBar +import androidx.appcompat.app.AppCompatActivity +import com.example.assignment.R +import com.example.assignment.util.Constant +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.OnMapReadyCallback +import com.google.android.gms.maps.SupportMapFragment +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.MarkerOptions + + +class MapActivity{ +// private var TAG: String? = null +// private var meteorName: String? = null +// private var lat = 0.0 +// private var lng = 0.0 +// var actionBar: ActionBar? = null +// var activityMapBinding: ActivityMapBinding? = null + +// override fun onCreate(savedInstanceState: Bundle?) { +// TAG = this.javaClass.name +// Log.d(TAG, "onCreate called") +// super.onCreate(savedInstanceState) +// activityMapBinding = ActivityMapBinding.inflate(getLayoutInflater()) +// setContentView(activityMapBinding!!.getRoot()) +// meteorName = intent.getStringExtra(Constant.Intent.METEORITE_NAME) +// lat = intent.getStringExtra(Constant.Intent.METEORITE_LAT)!!.toDouble() +// lng = intent.getStringExtra(Constant.Intent.METEORITE_LNG)!!.toDouble() +// val supportMapFragment = +// supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment? +// supportMapFragment!!.getMapAsync(this) +// actionBar = supportActionBar +// actionBar!!.setDisplayHomeAsUpEnabled(true) +// actionBar!!.setTitle(meteorName) +// } +// +// override fun onMapReady(googleMap: GoogleMap) { +// googleMap.mapType = GoogleMap.MAP_TYPE_HYBRID +// val latLng = LatLng(lat, lng) +// googleMap.addMarker( +// MarkerOptions() +// .position(latLng) +// .title(meteorName) +// ) +// googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, Constant.Map.CAMERA_ZOOM)) +// } +// +// override fun onOptionsItemSelected(item: MenuItem): Boolean { +// onBackPressed() +// return true +// } +} diff --git a/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt b/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt new file mode 100644 index 0000000..7c6ccef --- /dev/null +++ b/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt @@ -0,0 +1,53 @@ +package com.example.assignment.viewModel + +import android.content.Intent +import android.util.Log +import androidx.core.content.ContextCompat.startActivity +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.assignment.model.MeteorData +import com.example.assignment.network.MeteorsApi +import com.example.assignment.util.Constant +import com.example.assignment.view.MainActivity +import com.example.assignment.view.MapActivity +import kotlinx.coroutines.launch +import java.security.AccessController.getContext + +enum class MarsApiStatus { LOADING, ERROR, DONE } + +/** + * The [ViewModel] that is attached to the [MainActivity]. + */ +class MainViewModel : ViewModel() { + // Internally, we use a MutableLiveData, because we will be updating the List of MarsPhoto + // with new values + private val _meteors = MutableLiveData>() + + // The external LiveData interface to the property is immutable, so only this class can modify + val meteors: LiveData> = _meteors + + /** + * Call getInfo() on init so we can display status immediately. + */ + init { + getMeteorsInfo() + } + + /** + * Gets Mars photos information from the Mars API Retrofit service and updates the + * [MeteorData] [List] [LiveData]. + */ + private fun getMeteorsInfo() { + viewModelScope.launch { + try { + _meteors.value = MeteorsApi.retrofitService.getMeteorsInfo() + } catch (e: Exception) { + _meteors.value = listOf() + } + } + } + + fun itemClick(){} +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3f7768f..4cd4b3f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,23 +1,35 @@ - + - + + + + + android:layout_height="match_parent" + android:orientation="vertical" + tools:context=".view.MainActivity"> - + android:layout_height="wrap_content"> + + - + - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_map.xml b/app/src/main/res/layout/activity_map.xml index b544423..8ff8bbe 100644 --- a/app/src/main/res/layout/activity_map.xml +++ b/app/src/main/res/layout/activity_map.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.MapActivity"> + tools:context=".view.MapActivity"> - + xmlns:tools="http://schemas.android.com/tools"> - + + + + + android:layout_height="wrap_content" + android:layout_marginLeft="4dp" + android:layout_marginRight="4dp" + android:layout_marginTop="4dp" + android:foreground="?attr/selectableItemBackground" + card_view:cardCornerRadius="2dp"> - - + android:layout_weight="2" + android:orientation="vertical"> - + - + - + - + - + + + - + - \ No newline at end of file + + \ No newline at end of file From d52b47e6715e0b0205b62122adf105f188656811 Mon Sep 17 00:00:00 2001 From: Yu Xin Date: Thu, 24 Nov 2022 15:52:55 +0000 Subject: [PATCH 2/5] RecyclerView.Adapter --- .idea/misc.xml | 4 +- .../com/example/assignment/BindingAdapters.kt | 9 +- .../assignment/adapter/MeteorsListAdapter.kt | 93 +++++++------ .../example/assignment/model/MeteorData.kt | 2 +- .../assignment/network/MeteorApiService.kt | 4 +- .../example/assignment/view/MainActivity.kt | 20 +-- .../example/assignment/view/MapActivity.kt | 4 +- .../assignment/viewModel/MainViewModel.kt | 9 +- app/src/main/res/layout/activity_main.xml | 3 +- app/src/main/res/layout/item_meteorite.xml | 125 ++++++++---------- 10 files changed, 142 insertions(+), 131 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 9f0815d..ff666e7 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,10 +6,12 @@ - + + + diff --git a/app/src/main/java/com/example/assignment/BindingAdapters.kt b/app/src/main/java/com/example/assignment/BindingAdapters.kt index 80136f1..f71b8cc 100644 --- a/app/src/main/java/com/example/assignment/BindingAdapters.kt +++ b/app/src/main/java/com/example/assignment/BindingAdapters.kt @@ -1,5 +1,6 @@ package com.example.assignment +import android.util.Log import androidx.databinding.BindingAdapter import androidx.recyclerview.widget.RecyclerView import com.example.assignment.adapter.MeteorsListAdapter @@ -9,7 +10,11 @@ import com.example.assignment.model.MeteorData * Updates the data shown in the [RecyclerView]. */ @BindingAdapter("listData") -fun bindRecyclerView(recyclerView: RecyclerView, data: List?) { +fun bindRecyclerView(recyclerView: RecyclerView, data: MutableList?) { val adapter = recyclerView.adapter as MeteorsListAdapter - adapter.submitList(data) + if (data != null) { + adapter.submitList(data) + Log.d("Binding", data.toString()) + adapter.notifyDataSetChanged(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt b/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt index a3cb02b..eb000ab 100644 --- a/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt +++ b/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt @@ -1,68 +1,83 @@ package com.example.assignment.adapter +import android.content.Context +import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.example.assignment.databinding.ItemMeteoriteBinding import com.example.assignment.model.MeteorData - +import com.example.assignment.viewModel.MainViewModel /** - * This class implements a [RecyclerView] [ListAdapter] which uses Data Binding to present [List] - * data, including computing diffs between lists. + * This class implements a [RecyclerView] [ListAdapter] which uses Data Binding to present [List] data */ -class MeteorsListAdapter() : ListAdapter(DiffCallback) { +class MeteorsListAdapter( + context: Context, + recyclerItemClickListener: RecyclerItemClickListener, +) : RecyclerView.Adapter() { private val TAG: String = this.javaClass.name - /** - * The MarsPhotosViewHolder constructor takes the binding variable from the associated - * GridViewItem, which nicely gives it access to the full [MeteorData] information. - */ - class MeteorsViewHolder( - private var binding: ItemMeteoriteBinding - ) : RecyclerView.ViewHolder(binding.root) { + var meteorDataList: MutableList = mutableListOf() + var context: Context + var recyclerItemClickListener: RecyclerItemClickListener + + init { + this.context = context + this.recyclerItemClickListener = recyclerItemClickListener + } + + interface RecyclerItemClickListener { + fun onRecyclerItemClick(meteorItem: MeteorData?) + } + + class MeteorsViewHolder private constructor(private var binding: ItemMeteoriteBinding): RecyclerView.ViewHolder(binding.root) { fun bind(meteorData: MeteorData) { binding.meteor = meteorData // This is important, because it forces the data binding to execute immediately, // which allows the RecyclerView to make the correct view size measurements binding.executePendingBindings() } - } - - /** - * Allows the RecyclerView to determine which items have changed when the [List] of - * [MeteorData] has been updated. - */ - companion object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: MeteorData, newItem: MeteorData): Boolean { - return oldItem.id == newItem.id - } - override fun areContentsTheSame(oldItem: MeteorData, newItem: MeteorData): Boolean { - return oldItem.name == newItem.name + companion object { + fun from(parent: ViewGroup): MeteorsViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + val binding = ItemMeteoriteBinding.inflate(layoutInflater, parent, false) + return MeteorsViewHolder(binding) + } } } - /** - * Create new [RecyclerView] item views (invoked by the layout manager) - */ - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): MeteorsViewHolder { - return MeteorsViewHolder( - ItemMeteoriteBinding.inflate(LayoutInflater.from(parent.context)) - ) +// /** +// * Allows the RecyclerView to determine which items have changed when the [List] of +// * [MeteorData] has been updated. +// */ +// companion object DiffCallback : DiffUtil.ItemCallback() { +// override fun areItemsTheSame(oldItem: MeteorData, newItem: MeteorData): Boolean { +// return oldItem.id == newItem.id +// } +// +// override fun areContentsTheSame(oldItem: MeteorData, newItem: MeteorData): Boolean { +// return oldItem.name == newItem.name +// } +// } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MeteorsViewHolder { + return MeteorsViewHolder.from(parent) } - /** - * Replaces the contents of a view (invoked by the layout manager) - */ override fun onBindViewHolder(holder: MeteorsViewHolder, position: Int) { - val marsPhoto = getItem(position) - holder.bind(marsPhoto) + val meteor: MeteorData = meteorDataList[position] + holder.bind(meteor) + } + + override fun getItemCount(): Int { + return meteorDataList.size + } + + fun submitList(data: MutableList) { + meteorDataList = data } } \ No newline at end of file diff --git a/app/src/main/java/com/example/assignment/model/MeteorData.kt b/app/src/main/java/com/example/assignment/model/MeteorData.kt index 2d6401e..4dce413 100644 --- a/app/src/main/java/com/example/assignment/model/MeteorData.kt +++ b/app/src/main/java/com/example/assignment/model/MeteorData.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json data class MeteorData( - @Json(name = "id") var id: String? = null, + @Json(name = "id") val id: String? = null, @Json(name = "mass") var mass: Double = 0.0, @Json(name = "name") var name: String? = null, @Json(name = "reclat") var reclat: String? = null, diff --git a/app/src/main/java/com/example/assignment/network/MeteorApiService.kt b/app/src/main/java/com/example/assignment/network/MeteorApiService.kt index cf0e8e2..2bbc908 100644 --- a/app/src/main/java/com/example/assignment/network/MeteorApiService.kt +++ b/app/src/main/java/com/example/assignment/network/MeteorApiService.kt @@ -31,12 +31,12 @@ private val retrofit = Retrofit.Builder() */ interface MeteorsApiService { /** - * Returns a [List] of [MeteorData] and this method can be called from a Coroutine. + * Returns a [MutableList] of [MeteorData] and this method can be called from a Coroutine. * The @GET annotation indicates that the "photos" endpoint will be requested with the GET * HTTP method */ @GET("resource/y77d-th95.json?\$where=year>='1900-01-01T00:00:00.000'")//&\$limit=15") - suspend fun getMeteorsInfo(): List + suspend fun getMeteorsInfo(): MutableList } /** diff --git a/app/src/main/java/com/example/assignment/view/MainActivity.kt b/app/src/main/java/com/example/assignment/view/MainActivity.kt index 240edab..e93ac7b 100644 --- a/app/src/main/java/com/example/assignment/view/MainActivity.kt +++ b/app/src/main/java/com/example/assignment/view/MainActivity.kt @@ -18,7 +18,7 @@ import com.example.assignment.model.MeteorData import com.example.assignment.util.Constant import com.example.assignment.viewModel.MainViewModel -class MainActivity : AppCompatActivity(){ +class MainActivity : AppCompatActivity(), MeteorsListAdapter.RecyclerItemClickListener{ private val TAG: String = this.javaClass.name private val viewModel: MainViewModel by viewModels() lateinit var meteorsListAdapter: MeteorsListAdapter @@ -44,7 +44,7 @@ class MainActivity : AppCompatActivity(){ binding.recycler.setLayoutManager(layoutManager) // Sets the adapter of the photosGrid RecyclerView - meteorsListAdapter = MeteorsListAdapter() + meteorsListAdapter = MeteorsListAdapter(this,this) binding.recycler.setAdapter(meteorsListAdapter) } @@ -53,12 +53,12 @@ class MainActivity : AppCompatActivity(){ return true } -// override fun onRecyclerItemClick(meteorItem: MeteorData?) { -// Log.d(TAG, "onRecyclerItemClick called!!") -// val mapActivityIntent = Intent(this, MapActivity::class.java) -// mapActivityIntent.putExtra(Constant.Intent.METEORITE_NAME, meteorItem?.name) -// mapActivityIntent.putExtra(Constant.Intent.METEORITE_LAT, meteorItem?.reclat) -// mapActivityIntent.putExtra(Constant.Intent.METEORITE_LNG, meteorItem?.reclong) -// startActivity(mapActivityIntent) -// } + override fun onRecyclerItemClick(meteorItem: MeteorData?) { + Log.d(TAG, "onRecyclerItemClick called!!") + val mapActivityIntent = Intent(this, MapActivity::class.java) + mapActivityIntent.putExtra(Constant.Intent.METEORITE_NAME, meteorItem?.name) + mapActivityIntent.putExtra(Constant.Intent.METEORITE_LAT, meteorItem?.reclat) + mapActivityIntent.putExtra(Constant.Intent.METEORITE_LNG, meteorItem?.reclong) + startActivity(mapActivityIntent) + } } diff --git a/app/src/main/java/com/example/assignment/view/MapActivity.kt b/app/src/main/java/com/example/assignment/view/MapActivity.kt index d7be97f..f13520b 100644 --- a/app/src/main/java/com/example/assignment/view/MapActivity.kt +++ b/app/src/main/java/com/example/assignment/view/MapActivity.kt @@ -21,8 +21,8 @@ class MapActivity{ // private var lat = 0.0 // private var lng = 0.0 // var actionBar: ActionBar? = null -// var activityMapBinding: ActivityMapBinding? = null - +// var activityMapBinding: ac? = null +// // override fun onCreate(savedInstanceState: Bundle?) { // TAG = this.javaClass.name // Log.d(TAG, "onCreate called") diff --git a/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt b/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt index 7c6ccef..1a3c84e 100644 --- a/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt +++ b/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt @@ -21,12 +21,13 @@ enum class MarsApiStatus { LOADING, ERROR, DONE } * The [ViewModel] that is attached to the [MainActivity]. */ class MainViewModel : ViewModel() { + // Internally, we use a MutableLiveData, because we will be updating the List of MarsPhoto // with new values - private val _meteors = MutableLiveData>() + private val _meteors = MutableLiveData>() // The external LiveData interface to the property is immutable, so only this class can modify - val meteors: LiveData> = _meteors + val meteors: LiveData> = _meteors /** * Call getInfo() on init so we can display status immediately. @@ -44,10 +45,10 @@ class MainViewModel : ViewModel() { try { _meteors.value = MeteorsApi.retrofitService.getMeteorsInfo() } catch (e: Exception) { - _meteors.value = listOf() + _meteors.value = mutableListOf() } } } - fun itemClick(){} + } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4cd4b3f..593c898 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -26,8 +26,7 @@ android:layout_height="match_parent" android:scrollbars="vertical" android:visibility="visible" - app:listData="@{viewModel.meteors}" - /> + app:listData="@{viewModel.meteors}" /> diff --git a/app/src/main/res/layout/item_meteorite.xml b/app/src/main/res/layout/item_meteorite.xml index 9709e0c..69435f0 100644 --- a/app/src/main/res/layout/item_meteorite.xml +++ b/app/src/main/res/layout/item_meteorite.xml @@ -7,89 +7,78 @@ + + - - - - - + android:layout_height="69dp"> - + - - - - - - - + + - + + From 6b83fdf6bee4923d7c229272ffd60a148d05a38c Mon Sep 17 00:00:00 2001 From: Yu Xin Date: Fri, 25 Nov 2022 02:47:23 +0000 Subject: [PATCH 3/5] refresh + text formart binding --- .../com/example/assignment/BindingAdapters.kt | 25 ++++++++++++++++++- .../assignment/adapter/MeteorsListAdapter.kt | 1 - .../example/assignment/view/MainActivity.kt | 22 +++++++++++++++- .../assignment/viewModel/MainViewModel.kt | 6 ++++- app/src/main/res/layout/item_meteorite.xml | 5 ++-- 5 files changed, 53 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/example/assignment/BindingAdapters.kt b/app/src/main/java/com/example/assignment/BindingAdapters.kt index f71b8cc..2b53ee7 100644 --- a/app/src/main/java/com/example/assignment/BindingAdapters.kt +++ b/app/src/main/java/com/example/assignment/BindingAdapters.kt @@ -1,10 +1,14 @@ package com.example.assignment import android.util.Log +import android.widget.TextView import androidx.databinding.BindingAdapter import androidx.recyclerview.widget.RecyclerView import com.example.assignment.adapter.MeteorsListAdapter import com.example.assignment.model.MeteorData +import java.text.SimpleDateFormat +import java.util.* + /** * Updates the data shown in the [RecyclerView]. @@ -17,4 +21,23 @@ fun bindRecyclerView(recyclerView: RecyclerView, data: MutableList?) Log.d("Binding", data.toString()) adapter.notifyDataSetChanged(); } -} \ No newline at end of file +} + +/** + * Transform mass attribute from Double to String with kg. + */ +@BindingAdapter("app:massText") +fun massText(view: TextView, text: String) { + view.text = text + "kg" +} + +/** + * Transform mass attribute from Double to String with kg. + */ +@BindingAdapter("app:dateText") +fun dateText(view: TextView, text: String) { + val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss") + val date: Date = format.parse(text) + view.text = date.toString() +} + diff --git a/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt b/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt index eb000ab..9945e11 100644 --- a/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt +++ b/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt @@ -8,7 +8,6 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.example.assignment.databinding.ItemMeteoriteBinding import com.example.assignment.model.MeteorData -import com.example.assignment.viewModel.MainViewModel /** * This class implements a [RecyclerView] [ListAdapter] which uses Data Binding to present [List] data diff --git a/app/src/main/java/com/example/assignment/view/MainActivity.kt b/app/src/main/java/com/example/assignment/view/MainActivity.kt index e93ac7b..a350794 100644 --- a/app/src/main/java/com/example/assignment/view/MainActivity.kt +++ b/app/src/main/java/com/example/assignment/view/MainActivity.kt @@ -5,8 +5,10 @@ import android.content.Intent import android.os.Bundle import android.util.Log import android.view.Menu +import android.view.MenuItem import android.view.View import android.widget.AdapterView +import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager @@ -34,7 +36,7 @@ class MainActivity : AppCompatActivity(), MeteorsListAdapter.RecyclerItemClickLi val view: View = binding.getRoot() setContentView(view) - // Allows Data Binding to Observe LiveData with the lifecycle of this Fragment + // Allows Data Binding to Observe LiveData with the lifecycle of this Activity binding.lifecycleOwner = this // Giving the binding access to the OverviewViewModel @@ -53,6 +55,11 @@ class MainActivity : AppCompatActivity(), MeteorsListAdapter.RecyclerItemClickLi return true } + override fun onResume() { + super.onResume() + viewModel.refresh() + } + override fun onRecyclerItemClick(meteorItem: MeteorData?) { Log.d(TAG, "onRecyclerItemClick called!!") val mapActivityIntent = Intent(this, MapActivity::class.java) @@ -61,4 +68,17 @@ class MainActivity : AppCompatActivity(), MeteorsListAdapter.RecyclerItemClickLi mapActivityIntent.putExtra(Constant.Intent.METEORITE_LNG, meteorItem?.reclong) startActivity(mapActivityIntent) } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_sort -> { + Log.d(TAG,"TODO") + } + R.id.action_refresh -> { + viewModel.refresh() + Toast.makeText(this, "List refreshed", Toast.LENGTH_SHORT).show() + } + } + return true + } } diff --git a/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt b/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt index 1a3c84e..24e9418 100644 --- a/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt +++ b/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt @@ -37,7 +37,7 @@ class MainViewModel : ViewModel() { } /** - * Gets Mars photos information from the Mars API Retrofit service and updates the + * Gets Meteors photos information from the NASA's API Retrofit service and updates the * [MeteorData] [List] [LiveData]. */ private fun getMeteorsInfo() { @@ -50,5 +50,9 @@ class MainViewModel : ViewModel() { } } + fun refresh(){ + getMeteorsInfo() + } + } \ No newline at end of file diff --git a/app/src/main/res/layout/item_meteorite.xml b/app/src/main/res/layout/item_meteorite.xml index 69435f0..6fb37a5 100644 --- a/app/src/main/res/layout/item_meteorite.xml +++ b/app/src/main/res/layout/item_meteorite.xml @@ -1,6 +1,7 @@ @@ -47,7 +48,7 @@ android:layout_height="wrap_content" android:layout_marginTop="5dp" android:layout_marginEnd="10dp" - android:text="@{String.valueOf((float) meteor.mass)}" + app:massText="@{String.valueOf(meteor.mass)}" android:textAlignment="viewEnd" card_view:layout_constraintEnd_toEndOf="parent" card_view:layout_constraintTop_toTopOf="parent" @@ -74,7 +75,7 @@ android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginEnd="10dp" - android:text="@{meteor.year}" + app:dateText="@{meteor.year}" android:textAlignment="viewEnd" card_view:layout_constraintEnd_toEndOf="parent" card_view:layout_constraintTop_toBottomOf="@+id/mass" /> From 42f8e9ae6494a2b0551297af837c48da1cb6f53a Mon Sep 17 00:00:00 2001 From: Yu Xin Date: Thu, 1 Dec 2022 05:54:22 +0000 Subject: [PATCH 4/5] reply and action on comments --- .idea/misc.xml | 2 +- .../com/example/assignment/BindingAdapters.kt | 15 +++++++++++--- .../assignment/adapter/MeteorsListAdapter.kt | 12 +++-------- .../example/assignment/model/MeteorData.kt | 10 +++++----- .../example/assignment/view/MainActivity.kt | 14 ++++++------- app/src/main/res/layout/item_meteorite.xml | 20 ++++++++++--------- app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles.xml | 1 + 8 files changed, 40 insertions(+), 35 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index ff666e7..4c54e75 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,7 +6,7 @@ - + diff --git a/app/src/main/java/com/example/assignment/BindingAdapters.kt b/app/src/main/java/com/example/assignment/BindingAdapters.kt index 2b53ee7..2895231 100644 --- a/app/src/main/java/com/example/assignment/BindingAdapters.kt +++ b/app/src/main/java/com/example/assignment/BindingAdapters.kt @@ -1,5 +1,6 @@ package com.example.assignment +import android.provider.Settings.Global.getString import android.util.Log import android.widget.TextView import androidx.databinding.BindingAdapter @@ -27,12 +28,12 @@ fun bindRecyclerView(recyclerView: RecyclerView, data: MutableList?) * Transform mass attribute from Double to String with kg. */ @BindingAdapter("app:massText") -fun massText(view: TextView, text: String) { - view.text = text + "kg" +fun massText(view: TextView, mass: Double) { + view.text = String.format("%.1f", mass) + R.string.main_mass_unit } /** - * Transform mass attribute from Double to String with kg. + * Transform date format. */ @BindingAdapter("app:dateText") fun dateText(view: TextView, text: String) { @@ -41,3 +42,11 @@ fun dateText(view: TextView, text: String) { view.text = date.toString() } +/** + * Combine reclat and reclong together + */ +@BindingAdapter("app:locationText") +fun locationText(view: TextView, meteor: MeteorData) { + view.text ='[' + meteor.reclat.toString() + ',' + meteor.reclong.toString() + ']' +} + diff --git a/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt b/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt index 9945e11..0ead94b 100644 --- a/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt +++ b/app/src/main/java/com/example/assignment/adapter/MeteorsListAdapter.kt @@ -13,23 +13,17 @@ import com.example.assignment.model.MeteorData * This class implements a [RecyclerView] [ListAdapter] which uses Data Binding to present [List] data */ class MeteorsListAdapter( - context: Context, - recyclerItemClickListener: RecyclerItemClickListener, + context: Context ) : RecyclerView.Adapter() { private val TAG: String = this.javaClass.name var meteorDataList: MutableList = mutableListOf() - var context: Context - var recyclerItemClickListener: RecyclerItemClickListener + var context: Context = context - init { - this.context = context - this.recyclerItemClickListener = recyclerItemClickListener - } interface RecyclerItemClickListener { - fun onRecyclerItemClick(meteorItem: MeteorData?) + fun onRecyclerItemClick(meteorItem: MeteorData) } class MeteorsViewHolder private constructor(private var binding: ItemMeteoriteBinding): RecyclerView.ViewHolder(binding.root) { diff --git a/app/src/main/java/com/example/assignment/model/MeteorData.kt b/app/src/main/java/com/example/assignment/model/MeteorData.kt index 4dce413..fe1e88c 100644 --- a/app/src/main/java/com/example/assignment/model/MeteorData.kt +++ b/app/src/main/java/com/example/assignment/model/MeteorData.kt @@ -5,9 +5,9 @@ import com.squareup.moshi.Json data class MeteorData( @Json(name = "id") val id: String? = null, - @Json(name = "mass") var mass: Double = 0.0, - @Json(name = "name") var name: String? = null, - @Json(name = "reclat") var reclat: String? = null, - @Json(name = "reclong") var reclong: String? = null, - @Json(name = "year") var year: String? = null, + @Json(name = "mass") val mass: Double = 0.0, + @Json(name = "name") val name: String? = null, + @Json(name = "reclat") val reclat: String? = null, + @Json(name = "reclong") val reclong: String? = null, + @Json(name = "year") val year: String? = null, ) \ No newline at end of file diff --git a/app/src/main/java/com/example/assignment/view/MainActivity.kt b/app/src/main/java/com/example/assignment/view/MainActivity.kt index a350794..47e1fdf 100644 --- a/app/src/main/java/com/example/assignment/view/MainActivity.kt +++ b/app/src/main/java/com/example/assignment/view/MainActivity.kt @@ -7,15 +7,14 @@ import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.View -import android.widget.AdapterView import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.example.assignment.databinding.ActivityMainBinding import com.example.assignment.R import com.example.assignment.adapter.MeteorsListAdapter +import com.example.assignment.databinding.ActivityMainBinding import com.example.assignment.model.MeteorData import com.example.assignment.util.Constant import com.example.assignment.viewModel.MainViewModel @@ -46,7 +45,7 @@ class MainActivity : AppCompatActivity(), MeteorsListAdapter.RecyclerItemClickLi binding.recycler.setLayoutManager(layoutManager) // Sets the adapter of the photosGrid RecyclerView - meteorsListAdapter = MeteorsListAdapter(this,this) + meteorsListAdapter = MeteorsListAdapter(this) binding.recycler.setAdapter(meteorsListAdapter) } @@ -60,12 +59,11 @@ class MainActivity : AppCompatActivity(), MeteorsListAdapter.RecyclerItemClickLi viewModel.refresh() } - override fun onRecyclerItemClick(meteorItem: MeteorData?) { - Log.d(TAG, "onRecyclerItemClick called!!") + override fun onRecyclerItemClick(meteorItem: MeteorData) { val mapActivityIntent = Intent(this, MapActivity::class.java) - mapActivityIntent.putExtra(Constant.Intent.METEORITE_NAME, meteorItem?.name) - mapActivityIntent.putExtra(Constant.Intent.METEORITE_LAT, meteorItem?.reclat) - mapActivityIntent.putExtra(Constant.Intent.METEORITE_LNG, meteorItem?.reclong) + mapActivityIntent.putExtra(Constant.Intent.METEORITE_NAME, meteorItem.name) + mapActivityIntent.putExtra(Constant.Intent.METEORITE_LAT, meteorItem.reclat) + mapActivityIntent.putExtra(Constant.Intent.METEORITE_LNG, meteorItem.reclong) startActivity(mapActivityIntent) } diff --git a/app/src/main/res/layout/item_meteorite.xml b/app/src/main/res/layout/item_meteorite.xml index 6fb37a5..1829046 100644 --- a/app/src/main/res/layout/item_meteorite.xml +++ b/app/src/main/res/layout/item_meteorite.xml @@ -26,7 +26,7 @@ + android:layout_height="103dp"> @@ -44,12 +43,12 @@ @@ -63,7 +62,7 @@ android:layout_marginTop="10dp" android:paddingStart="15dp" android:paddingEnd="4dp" - android:text="@{ '[' + meteor.reclat + ',' + meteor.reclong + ']' }" + app:locationText="@{meteor}" card_view:layout_constraintStart_toStartOf="parent" card_view:layout_constraintTop_toBottomOf="@+id/name" tools:text="[54.0000420, 22:005000]" /> @@ -73,12 +72,15 @@ style="@style/RecyclerText" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginStart="10dp" android:layout_marginTop="10dp" - android:layout_marginEnd="10dp" - app:dateText="@{meteor.year}" android:textAlignment="viewEnd" + app:dateText="@{meteor.year}" card_view:layout_constraintEnd_toEndOf="parent" - card_view:layout_constraintTop_toBottomOf="@+id/mass" /> + card_view:layout_constraintHorizontal_bias="0.0" + card_view:layout_constraintStart_toStartOf="parent" + card_view:layout_constraintTop_toBottomOf="@+id/location" + tools:text="Tue Jan 01 00:00:00 GMT 1952" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c42b56c..c01d0b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ Cancel Ascending Descending + kg No data available, sorting is disabled diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ca2774f..cd21b0b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -15,5 +15,6 @@ \ No newline at end of file From 77578968031048e8413932ce18b10c3bbbf18d1c Mon Sep 17 00:00:00 2001 From: Yu Xin Date: Thu, 15 Dec 2022 11:12:22 +0000 Subject: [PATCH 5/5] resolve milestone2 comments --- .../java/com/example/assignment/view/MainActivity.kt | 6 +----- .../example/assignment/viewModel/MainViewModel.kt | 12 +++--------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/example/assignment/view/MainActivity.kt b/app/src/main/java/com/example/assignment/view/MainActivity.kt index 47e1fdf..353712f 100644 --- a/app/src/main/java/com/example/assignment/view/MainActivity.kt +++ b/app/src/main/java/com/example/assignment/view/MainActivity.kt @@ -40,6 +40,7 @@ class MainActivity : AppCompatActivity(), MeteorsListAdapter.RecyclerItemClickLi // Giving the binding access to the OverviewViewModel binding.viewModel = viewModel + lifecycle.addObserver(viewModel) val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this) binding.recycler.setLayoutManager(layoutManager) @@ -54,11 +55,6 @@ class MainActivity : AppCompatActivity(), MeteorsListAdapter.RecyclerItemClickLi return true } - override fun onResume() { - super.onResume() - viewModel.refresh() - } - override fun onRecyclerItemClick(meteorItem: MeteorData) { val mapActivityIntent = Intent(this, MapActivity::class.java) mapActivityIntent.putExtra(Constant.Intent.METEORITE_NAME, meteorItem.name) diff --git a/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt b/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt index 24e9418..2a4759a 100644 --- a/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt +++ b/app/src/main/java/com/example/assignment/viewModel/MainViewModel.kt @@ -3,10 +3,7 @@ package com.example.assignment.viewModel import android.content.Intent import android.util.Log import androidx.core.content.ContextCompat.startActivity -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* import com.example.assignment.model.MeteorData import com.example.assignment.network.MeteorsApi import com.example.assignment.util.Constant @@ -20,7 +17,7 @@ enum class MarsApiStatus { LOADING, ERROR, DONE } /** * The [ViewModel] that is attached to the [MainActivity]. */ -class MainViewModel : ViewModel() { +class MainViewModel : ViewModel(), DefaultLifecycleObserver { // Internally, we use a MutableLiveData, because we will be updating the List of MarsPhoto // with new values @@ -29,10 +26,7 @@ class MainViewModel : ViewModel() { // The external LiveData interface to the property is immutable, so only this class can modify val meteors: LiveData> = _meteors - /** - * Call getInfo() on init so we can display status immediately. - */ - init { + override fun onResume(owner: LifecycleOwner) { getMeteorsInfo() }