Skip to content

Instantly share code, notes, and snippets.

@nglauber
Created January 9, 2018 17:16
Show Gist options
  • Save nglauber/616563985a6a05830eb4ae4d086aa4bc to your computer and use it in GitHub Desktop.
Save nglauber/616563985a6a05830eb4ae4d086aa4bc to your computer and use it in GitHub Desktop.
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
defaultConfig {
applicationId "br.com.nglauber.marvel"
minSdkVersion 19
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.19.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.19.3'
implementation 'ru.gildor.coroutines:kotlin-coroutines-retrofit:0.8.2'
implementation "com.android.support:appcompat-v7:$appcompat_version"
implementation "com.android.support:recyclerview-v7:$appcompat_version"
implementation "com.android.support.constraint:constraint-layout:$constraintlayout_version"
implementation "android.arch.lifecycle:extensions:1.0.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
testImplementation 'junit:junit:4.12'
}
repositories {
mavenCentral()
}
buildscript {
ext.kotlin_version = '1.2.10'
ext.appcompat_version = '27.0.1'
ext.constraintlayout_version = '1.0.2'
ext.rxjava2_version = '2.1.3'
ext.rxandroid_version = '2.0.1'
ext.retrofit_version = '2.3.0'
ext.okhttp_version = '3.6.0'
ext.glide_version = '4.3.1'
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
jcenter()
google()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModelProviders
import android.os.Bundle
import android.os.Parcelable
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.Log
import br.com.nglauber.marvel.R
import kotlinx.android.synthetic.main.activity_characters.*
class CharactersActivity : AppCompatActivity() {
private val viewModel: CharactersViewModel by lazy {
ViewModelProviders.of(this).get(CharactersViewModel::class.java)
}
private val adapter: CharactersAdapter by lazy {
CharactersAdapter()
}
private var recyclerState: Parcelable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_characters)
val observer = MyLifecycleObserver()
lifecycle.addObserver(observer)
viewModel.getCharacters().observe(this, Observer { characters ->
characters?.let {
adapter.setItems(characters)
}
if (recyclerState != null) {
recyclerCharacters.layoutManager.onRestoreInstanceState(recyclerState)
recyclerState = null
}
})
val llm = LinearLayoutManager(this)
recyclerCharacters.layoutManager = llm
recyclerCharacters.adapter = adapter
recyclerCharacters.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val lastVisibleItemPosition = llm.findLastVisibleItemPosition()
if (lastVisibleItemPosition == adapter.itemCount - 1 && !viewModel.isLoading) {
Log.d("NGVL", "Loading more...")
loadCharacters(viewModel.currentPage + 1)
}
}
})
loadCharacters(0)
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
outState?.putParcelable("lmState", recyclerCharacters.layoutManager.onSaveInstanceState())
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
super.onRestoreInstanceState(savedInstanceState)
recyclerState = savedInstanceState?.getParcelable("lmState")
}
private fun loadCharacters(page: Int) {
viewModel.load(page)
}
}
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import br.com.nglauber.marvel.R
import br.com.nglauber.marvel.extensions.load
import br.com.nglauber.marvel.model.api.entity.Character
import kotlinx.android.synthetic.main.item_character.view.*
class CharactersAdapter() : RecyclerView.Adapter<CharactersAdapter.VH>() {
private val items = mutableListOf<Character>()
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): VH {
val view = LayoutInflater.from(parent?.context).inflate(R.layout.item_character, parent, false)
return VH(view)
}
override fun onBindViewHolder(holder: VH, position: Int) {
val character = items[position]
holder.txtName.text = character.name
holder.imgThumbnail.load("${character.thumbnail.path}/standard_medium.${character.thumbnail.extension}")
}
override fun getItemCount(): Int = items.size
class VH(itemView: View) : RecyclerView.ViewHolder(itemView) {
val imgThumbnail = itemView.imgThumbnail
val txtName = itemView.txtName
}
fun setItems(characters: List<Character>) {
items.clear()
items.addAll(characters)
notifyDataSetChanged()
}
}
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.ViewModel
import br.com.nglauber.marvel.model.api.MarvelApi
import br.com.nglauber.marvel.model.api.entity.Character
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.launch
import ru.gildor.coroutines.retrofit.await
class CharactersViewModel : ViewModel() {
var isLoading: Boolean = false
private set
var currentPage = -1
private set
private val characters = MutableLiveData<List<Character>>()
fun getCharacters(): LiveData<List<Character>> {
return characters
}
init {
characters.value = emptyList()
}
fun load(page: Int) {
launch(UI) {
isLoading = true
if (page > currentPage) {
val result = MarvelApi.loadCharacters(page).await()
val list = characters.value?.toMutableList()
list?.addAll(result.data.results)
characters.value = list
}
isLoading = false
currentPage++
}
}
fun reset() {
isLoading = false
currentPage = -1
characters.value = emptyList()
}
}
data class Response(
val code: Int,
val etag: String,
val data: Data
)
data class Data(
val offset: Int,
val limit: Int,
val total: Int,
val count: Int,
val results: List<Character>
)
data class Character(
val id: Int,
val name: String,
val description: String,
val thumbnail: Thumbnail
)
data class Thumbnail(
val path: String,
val extension: String
)
import br.com.nglauber.marvel.extensions.md5
import br.com.nglauber.marvel.model.api.entity.API_KEY
import br.com.nglauber.marvel.model.api.entity.PRIVATE_KEY
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.*
object MarvelApi {
private val api: MarvelApiDef by lazy {
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
val httpClient = OkHttpClient.Builder()
httpClient.addInterceptor(logging)
httpClient.addInterceptor { chain ->
val original = chain.request()
val originalHttpUrl = original.url()
val ts = (Calendar.getInstance(TimeZone.getTimeZone("UTC")).timeInMillis / 1000L).toString()
val url = originalHttpUrl.newBuilder()
.addQueryParameter("apikey", API_KEY)
.addQueryParameter("ts", ts)
.addQueryParameter("hash", "$ts$PRIVATE_KEY$API_KEY".md5())
.build()
val requestBuilder = original.newBuilder().url(url)
val request = requestBuilder.build()
chain.proceed(request)
}
val gson = GsonBuilder().setLenient().create()
val retrofit = Retrofit.Builder()
.baseUrl("http://gateway.marvel.com/v1/public/")
.addConverterFactory(GsonConverterFactory.create(gson))
.client(httpClient.build())
.build()
retrofit.create<MarvelApiDef>(MarvelApiDef::class.java)
}
fun loadCharacters(page: Int) = api.allCharacters(page * 20)
}
import br.com.nglauber.marvel.model.api.entity.Response
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface MarvelApiDef {
@GET("characters")
fun allCharacters(@Query("offset") offset: Int? = 0): Call<Response>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment