Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 54 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,57 @@
# Yuki's Android Learning Plan guided by Sammy #
## Milestones
1. Bare bones application, including main components and architecture.
1. Bare bones application, including main components and architecture.
* Appropriate choice of architecture.
* Defining components that are needed and how they fit into the architecture.
2. API integration
* Implementation of network layer
3. Displaying list of meteors
* Using appropriate Android components, display a list of meteors received from the API (point 2).
* Defining components that are needed and how they fit into the architecture.
2. API integration
* Implementation of network layer
3. Displaying list of meteors
* Using appropriate Android components, display a list of meteors received from the API (point 2).
4. List filtering and sorting
* Add interactive filter & sorting UI components to the view
* Display filtered/sorted list
5. Detail view
* More complex view logic by adding a detail view and handling navigation
6. Persistence
* List of meteors should be stored for offline usage.
* Update application logic to use offline state store whenever API is not available.

## Comments Summary
1. ListAdapter vs. RecyclerViewAdapter -> RecyclerViewAdapter
2. Lifecycle-Aware Components -> lifecycle
```MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
lifecycle.addObserver(viewModel)
}
```
```MainViewModel.kt
private val repository: MeteorRepository) : ViewModel(), DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
getMeteorsInfo()
}
}
```

3. Prevent leak -> onDestroy()
```MainActivity
override fun onDestroy() {
super.onDestroy()
Injection.destroy()
lifecycle.removeObserver(viewModel)}
```
"This is a code vulnerability. You're setting the entire Activity as the listener, which means that the Activity can't be garbage collected as long as there is a reference to it. When is the reference to the activity cleaned up? Furthermore, the Activity doesn't really need to be informed of clicks when the activity is no longer in view. So: it's a good practise to "unset" the listener, for example in 'onDestroy()'."

4. RelativeLayout vs. LinearLayout -> RelativeLayout

5. Don't use hardcoded strings, use string resources instead. -> R.string.name

6. MVVM architecture (especially the functionality of ViewModel)

7. val vs. var -> val

8. unnecessary init{} -> declaration with initialization
“Declare context and recyclerItemClickListener as private vals in the MeteorsListAdapter initialisor, and you can use them as variables in the class. ”

9. MutableList vs. List -> List<MeteorData>

10. databinding
28 changes: 20 additions & 8 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,35 @@ 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.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.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'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.fragment:fragment-ktx:1.5.5'

implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.google.android.gms:play-services-basement:18.1.0'
implementation 'com.google.android.gms:play-services-maps:18.1.0'

implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation "com.squareup.okhttp3:logging-interceptor:4.10.0"

implementation 'com.orhanobut:hawk:2.0.1'

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'

}
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBBuSbrTz9SuyFwci1bCzyO0ZLPspoP2os"/>

<activity android:name=".ui.MainActivity"
<activity android:name=".view.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand All @@ -26,7 +26,7 @@
</intent-filter>
</activity>
<activity
android:name=".ui.MapActivity"
android:name=".view.MapActivity"
android:screenOrientation="portrait">
</activity>
</application>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,16 @@ 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

import com.orhanobut.hawk.Hawk.init

class MeteorsListAdapter(
context: Context,
recyclerItemClickListener: RecyclerItemClickListener
meteorList: List<MeteorData>,
var context: Context,
var recyclerItemClickListener: RecyclerItemClickListener
) : RecyclerView.Adapter<MeteorsListAdapter.MeteorsViewHolder>() {

private val TAG: String = this.javaClass.name
var meteorDataList: MutableList<MeteorData>
var context: Context
var recyclerItemClickListener: RecyclerItemClickListener

init {
this.context = context
meteorDataList = parseJsonToObject(context) as MutableList<MeteorData>
Log.d(TAG, meteorDataList.toString())
this.recyclerItemClickListener = recyclerItemClickListener
}
var meteorDataList: List<MeteorData> = meteorList

override fun onCreateViewHolder(
parent: ViewGroup,
Expand All @@ -54,28 +43,17 @@ class MeteorsListAdapter(
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)
}
fun update(data: List<MeteorData>) {
meteorDataList = data
notifyDataSetChanged()
}

private fun getItem(position: Int): MeteorData {
return meteorDataList[position]
}

fun clear() {
Log.d(TAG, "clear Recycler view.")
while (itemCount > 0) {
remove(getItem(0))
}
}

interface RecyclerItemClickListener {
fun onRecyclerItemClick(meteorItem: MeteorData?)
fun onRecyclerItemClick(meteorItem: MeteorData)
}

inner class MeteorsViewHolder(itemBinding: ItemMeteoriteBinding) :
Expand All @@ -97,28 +75,4 @@ class MeteorsListAdapter(
})
}
}

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
}

private fun parseJsonToObject(context: Context): List<MeteorData> {
val jsonString: String? = loadJSONFromAsset(context)
return Gson().fromJson(jsonString, Array<MeteorData>::class.java).toList()
}



}
44 changes: 44 additions & 0 deletions app/src/main/java/com/example/assignment/data/ApiClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.example.assignment.data

import com.example.assignment.model.MeteorData
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET


object ApiClient {

private const val API_BASE_URL = "https://data.nasa.gov/"

private var servicesApiInterface: MeteorApiInterface? = null

fun build(): MeteorApiInterface {
val builder: Retrofit.Builder = Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())

val httpClient: OkHttpClient.Builder = OkHttpClient.Builder()
httpClient.addInterceptor(interceptor())

val retrofit: Retrofit = builder.client(httpClient.build()).build()
servicesApiInterface = retrofit.create(
MeteorApiInterface::class.java
)

return servicesApiInterface as MeteorApiInterface
}

private fun interceptor(): HttpLoggingInterceptor {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
return httpLoggingInterceptor
}

interface MeteorApiInterface {
@GET("resource/y77d-th95.json?\$where=year>='1900-01-01T00:00:00.000'")//&\$limit=15")
fun getMeteorsJsonData(): Call<List<MeteorData>>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.example.assignment.data

import com.example.assignment.model.MeteorData
import com.example.assignment.model.MeteorDataSource
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response


class MeteorRemoteDataSource(apiClient: ApiClient) : MeteorDataSource {

private var call: Call<List<MeteorData>>? = null
private val service = apiClient.build()

override fun retrieveMeteor(callback: OperationCallback<MeteorData>) {
call = service.getMeteorsJsonData()
call?.enqueue(object : Callback<List<MeteorData>> {
override fun onFailure(call: Call<List<MeteorData>>, t: Throwable) {
callback.onError(t.message)
}

override fun onResponse(
call: Call<List<MeteorData>>,
response: Response<List<MeteorData>>
) {
response.body()?.let {
if (response.isSuccessful) {
callback.onSuccess(it)
} else {
callback.onError("Error")
}
}
}
})
}

override fun cancel() {
call?.let {
it.cancel()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.assignment.data


interface OperationCallback<T> {
fun onSuccess(data: List<T>?)
fun onError(error: String?)
}
44 changes: 44 additions & 0 deletions app/src/main/java/com/example/assignment/di/injection.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.example.assignment.di

import android.content.Context
import com.example.assignment.data.ApiClient
import com.example.assignment.data.MeteorRemoteDataSource
import com.example.assignment.model.MeteorDataSource
import com.example.assignment.model.MeteorRepository
import com.example.assignment.viewModel.ViewModelFactory

object Injection {

private var meteorDataSource: MeteorDataSource? = null
private var meteorRepository: MeteorRepository? = null
private var meteorViewModelFactory: ViewModelFactory? = null

private fun createMeteorDataSource(): MeteorDataSource {
val dataSource = MeteorRemoteDataSource(ApiClient)
meteorDataSource = dataSource
return dataSource
}

private fun createMeteorRepository(): MeteorRepository {
val repository = MeteorRepository(provideDataSource())
meteorRepository = repository
return repository
}

private fun createFactory(): ViewModelFactory {
val factory = ViewModelFactory(providerRepository())
meteorViewModelFactory = factory
return factory
}

private fun provideDataSource() = meteorDataSource ?: createMeteorDataSource()
private fun providerRepository() = meteorRepository ?: createMeteorRepository()

fun provideViewModelFactory() = meteorViewModelFactory ?: createFactory()

fun destroy() {
meteorDataSource = null
meteorRepository = null
meteorViewModelFactory = null
}
}
Loading