Ejercicio con Retrofit: Repositorios en Kotlin
Obtener repositorios de un usuario en GitHub usando Kotlin
Ejemplo: Tutorial de Retrofit en Kotlin
Crear una aplicación que pida un usuario y obtenga todos sus repositorios en Github
https://api.github.com/users/paco-portada/repos
Se mostrará una lista (usando un RecyclerView) con los nombres, descripciones y fechas de creación de todos sus repositorios públicos.
Cuando se pulse en un elemento de la lista, se abrirá el navegador para mostrar el repositorio elegido.
¿Esquema de red?
Creación de la aplicación con Android Studio: RepositoriosKotlin
– Dar permisos en el manifiesto
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.repositorios">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:usesCleartextTraffic="true"
</application>
</manifest>
– Usar view binding
build.gradle:
buildFeatures {
viewBinding = true
}
– Añadir dependencias:
versión antigua:
implementation ("androidx.recyclerview:recyclerview:1.3.2")
implementation ("androidx.cardview:cardview:1.0.0")
// https://square.github.io/okhttp/#releases
implementation ("com.squareup.okhttp3:okhttp:4.12.0")
// https://github.com/square/retrofit
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
// https://github.com/google/gson
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
Reemplazar por la nueva declaración:
implementation (libs.androidx.recyclerview) implementation (libs.androidx.cardview) // https://square.github.io/okhttp/#releases implementation (libs.okhttp) // https://github.com/square/retrofit implementation (libs.retrofit) // https://github.com/google/gson implementation (libs.converter.gson) implementation (libs.kotlinx.coroutines.core) implementation (libs.androidx.lifecycle.runtime.ktx)
– Repositorios en GitHub
https://api.github.com/users/paco-portada/repos
Generate Plain Old Java Objects from JSON or JSON-Schema: jsonschema2pojo
Otra forma de obtener las clases: JSON to Kotlin Data Class Generator Online
Otra forma de obtener las clases en Kotlin: Plugin JSon to Kotlin Class
clase Owner?
data class Owner (
@SerializedName("login")
var login: String,
@SerializedName("avatar_url")
var avatarUrl: String
)
clase Repo?
data class Repo (
@SerializedName("name")
var name: String,
@SerializedName("owner")
var owner: Owner,
@SerializedName("html_url")
var htmlUrl: String,
@SerializedName("description")
var description: String,
@SerializedName("created_at")
var createdAt: String
)
– Creación del layout
Floating Action Button:
/res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
</resources>
/res/values/strings.xml
<resources>
<string name="app_name">Repositorios Kotlin</string>
<string name="name">Name</string>
<string name="description">Description</string>
<string name="createdat">CreatedAt</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/viewRoot"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.SearchView
android:id="@+id/svRepos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="7dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintVertical_bias="0.0"
app:queryHint="paco-portada, ArisGuimera"/>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinatorLayout"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/svRepos">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvRepos"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="list of owner repos in Github" >
</androidx.recyclerview.widget.RecyclerView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="24dp"
android:src="@drawable/baseline_autorenew_24"
app:layout_anchor="@id/rvRepos"
app:layout_anchorGravity="bottom|right|end"
android:contentDescription="click to get repos" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
repo_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/name"
android:textAppearance="@android:style/TextAppearance.Material.Large"
android:textColor="@color/teal_700" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/description"
android:textColor="@color/purple_500" />
<TextView
android:id="@+id/tvCreatedAt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/createdat"
android:textColor="@color/teal_200" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
– Creación del RecyclerView y el adapter
ReposAdapter.java
class ReposAdapter(private val reposList : List<Repo>) : RecyclerView.Adapter<ReposAdapter.RepoViewHolder>() {
lateinit var onItemClick: ((Repo) -> Unit)
lateinit var onLongItemClick: ((Repo) -> Unit)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RepoViewHolder {
val itemBinding = RepoItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return RepoViewHolder(itemBinding)
}
override fun getItemCount(): Int = reposList.size
override fun onBindViewHolder(holder: RepoViewHolder, position: Int) {
val item = reposList[position]
holder.render(item)
}
inner class RepoViewHolder(binding: RepoItemBinding) : RecyclerView.ViewHolder(binding.root) {
private val name = binding.tvName
private val description = binding.tvDescription
private val date = binding.tvCreatedAt
init {
itemView.setOnClickListener {
onItemClick.invoke(reposList[layoutPosition])
}
itemView.setOnLongClickListener {
onLongItemClick.invoke(reposList[layoutPosition])
false
}
}
fun render(repo: Repo) {
name.text = repo.name
description.text = repo.description
date.text = repo.createdAt
}
}
}
Patrón Singleton
– Retrofit
| Consuming APIs with Retrofit
Cómo consumir una API y procesar la respuesta usando Retrofit
|
![]() |
ApiService.java
interface ApiService {
// No usar suspend con Call
// https://stackoverflow.com/questions/75139082/jsonioexception-interfaces-cant-be-instantiated-register-an-instancecreator-o
// @GET("users/{username}/repos")
// fun listRepos(@Path("username") username: String?): Call<ArrayList<Repo>>
@GET("users/{username}/repos")
suspend fun listRepos(@Path("username") username: String?): Response<ArrayList<Repo>>
// @GET("ficheros/repos.json")
// fun getRepos(): Call<ArrayList<Repo>>
@GET("ficheros/repos.json")
suspend fun getRepos(): Response<ArrayList<Repo>>
}
Creating the Retrofit instance:
ApiAdapter.java
object ApiAdapter {
private var API_SERVICE: ApiService? = null
const val BASE_URL = "https://api.github.com/"
//const val BASE_URL = "https://api.githubmal.com/"
// const val BASE_URL = "https://alumno.me/"
// ip del servidor local
// const val BASE_URL = "http://192.168.100.50/"
// Añadir al manifiesto
// android:usesCleartextTraffic="true"
@get:Synchronized
val instance: ApiService?
get() {
if (API_SERVICE == null) {
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build()
val gson = GsonBuilder()
.setDateFormat("dd-MM-yyyy")
.create()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build()
API_SERVICE = retrofit.create(ApiService::class.java)
}
return API_SERVICE
}
}
Android Kotlin Coroutine with Retrofit
lifecycleScope.launch { val response = retrofit.getUsers() if (response.isSuccessful) { launch(Dispatchers.Main) { if (!response.body().isNullOrEmpty()) { val recyclerAdapter = response.body()?.let { RecyclerAdapter(it) } binding.recyclerView.adapter = recyclerAdapter } } } }
MainActivity.java
class MainActivity : AppCompatActivity(), View.OnClickListener, SearchView.OnQueryTextListener {
private lateinit var binding: ActivityMainBinding
private lateinit var adapter: ReposAdapter
private val listRepos = mutableListOf<Repo>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.svRepos.setOnQueryTextListener(this)
binding.fab.setOnClickListener(this)
initRecyclerView()
// getRepos()
}
private fun initRecyclerView() {
adapter = ReposAdapter(listRepos)
// binding.rvDogs.setHasFixedSize(true)
binding.rvRepos.layoutManager = LinearLayoutManager(this)
binding.rvRepos.adapter = adapter
adapter.onItemClick = {
showMessage("Single Click: \n" + it.htmlUrl)
// val uri = Uri.parse(it.htmlUrl)
val uri = it.htmlUrl.toUri()
val intent = Intent(Intent.ACTION_VIEW, uri)
// if (intent.resolveActivity(packageManager) != null)
startActivity(intent)
// else
// showMessage("No hay un navegador")
}
adapter.onLongItemClick = {
showMessage("Long Click: ${it.name}")
}
}
override fun onClick(v: View?) {
if (v === binding.fab) {
val user = binding.svRepos.query.toString()
if (user.isEmpty())
showMessage("Introduzca un usuario")
else
searchByName(user.lowercase())
}
}
/*
private fun searchByName(query: String) {
lateinit var call: Call<ArrayList<Repo>>
CoroutineScope(Dispatchers.IO).launch {
call = instance!!.listRepos(query)
call.enqueue(object : Callback<ArrayList<Repo>> {
override fun onResponse(
call: Call<ArrayList<Repo>>,
response: Response<ArrayList<Repo>>
) {
runOnUiThread {
hideKeyboard()
if (response.isSuccessful) {
// Handle the retrieved post data
val repos: ArrayList<Repo> =
(response.body() ?: emptyArray<Repo>()) as ArrayList<Repo>
listRepos.clear()
listRepos.addAll(repos)
adapter.notifyDataSetChanged()
} else {
// Handle error
showError(response.code().toString())
}
}
}
override fun onFailure(call: Call<ArrayList<Repo>>, t: Throwable) {
// Handle failure
runOnUiThread {
showMessage(t.toString())
}
}
})
}
}
*/
private fun searchByName(query: String) {
//CoroutineScope(Dispatchers.IO).launch {
lifecycleScope.launch {
try {
var response =
// petición a Github
// instance!!.listRepos(query)
// petición a un servidor local
instance!!.getRepos()
//runOnUiThread {
launch(Dispatchers.Main) {
hideKeyboard()
if (response.isSuccessful) {
// Handle the retrieved post data
val repos: ArrayList<Repo> =
(response.body() ?: emptyArray<Repo>()) as ArrayList<Repo>
listRepos.clear()
listRepos.addAll(repos)
adapter.notifyDataSetChanged()
} else {
// Handle error
showError(response.code().toString())
}
}
} catch (e: Exception) {
launch(Dispatchers.Main) {
hideKeyboard()
showError("Error:\n" + e.message.toString())
Log.e("Error", e.message.toString())
}
}
}
}
private fun showMessage(message: String) {
Log.i("Info", message)
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
private fun showError(message: String) {
Log.e("Error", message)
Toast.makeText(this, "Error:\n $message", Toast.LENGTH_SHORT).show()
}
override fun onQueryTextSubmit(query: String): Boolean {
if (query.isNotEmpty())
searchByName(query.lowercase())
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
private fun hideKeyboard() {
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(binding.viewRoot.windowToken, 0)
}
}
Acceso a Github:
ApiAdapter
object ApiAdapter {
private var API_SERVICE: ApiService? = null
const val BASE_URL = "https://api.github.com/"
//const val BASE_URL = "https://api.githubmal.com/"
. . .
MainActivity
private fun searchByName(query: String) {
//CoroutineScope(Dispatchers.IO).launch {
lifecycleScope.launch {
try {
var response =
// petición a Github
instance!!.listRepos(query)
// petición a un servidor local
// instance!!.getRepos()
Acceso al servidor local o propio (alumno.me):
Añadir el fichero repos.json al servidor local en la carpeta ficheros
ssh usuario@alumno.me
cd /var/www/html sudo su mkdir ficheros cd ficheros nano repos.json
ApiAdapter
object ApiAdapter {
private var API_SERVICE: ApiService? = null
// const val BASE_URL = "https://api.github.com/"
//const val BASE_URL = "https://api.githubmal.com/"
const val BASE_URL = "https://alumno.me/"
// ip del servidor local
// const val BASE_URL = "http://192.168.100.50/"
// Añadir al manifiesto
// android:usesCleartextTraffic="true"
MainActivity
private fun searchByName(query: String) {
//CoroutineScope(Dispatchers.IO).launch {
lifecycleScope.launch {
try {
var response =
// petición a Github
// instance!!.listRepos(query)
// petición a un servidor local
instance!!.getRepos()
Errores:
– El servidor no responde
– No se encuentra el fichero
– Error de sintaxis en el archivo json




Deja una respuesta
Lo siento, debes estar conectado para publicar un comentario.