Ejercicio con Retrofit: Repositorios
Obtener repositorios de un usuario en GitHub
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 actualización de todos sus repositorios.
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:

– 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" />
<uses-permission-sdk-23 android:name="android.permission.INTERNET" />
<application
android:usesCleartextTraffic="true"
</application>
</manifest>
– Usar view binding
build.gradle:
buildFeatures {
viewBinding true
}
– Añadir dependencias:
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
// https://square.github.io/okhttp/#releases
implementation 'com.squareup.okhttp3:okhttp:4.10.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'
// https://github.com/material-components/material-components-android/releases
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
– Repositorios en GitHub
https://api.github.com/users/paco-portada/repos
Generate Plain Old Java Objects from JSON or JSON-Schema: jsonschema2pojo
clase Owner?
clase Repo?
– Creación del layout
Floating Action Button:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.repositorios.MainActivity">
<EditText
android:id="@+id/editText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="paco-portada"
app:layout_constraintBottom_toTopOf="@+id/coordinatorLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<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/editText">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</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/autorenew"
app:layout_anchor="@id/recyclerView"
app:layout_anchorGravity="bottom|right|end" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
/res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
item_view.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/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Name"
android:textAppearance="@android:style/TextAppearance.Material.Large"
android:textColor="@color/teal_700" />
<TextView
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="Description"
android:textColor="@color/purple_500" />
<TextView
android:id="@+id/textView3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="CreatedAt"
android:textColor="@color/teal_200" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
– Creación del RecyclerView y el adapter
Android – RecyclerView: Implementing single item click and long press
Interfaz ClickListener
public interface ClickListener{
public void onClick(View view, int position);
public void onLongClick(View view,int position);
}
RecyclerTouchListener.java
public class RecyclerTouchListener implements RecyclerView.OnItemTouchListener{
public ClickListener clicklistener;
private GestureDetector gestureDetector;
public RecyclerTouchListener(Context context, final RecyclerView recycleView, final ClickListener clicklistener){
this.clicklistener=clicklistener;
gestureDetector=new GestureDetector(context,new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onSingleTapUp(MotionEvent e) {
return true;
}
@Override
public void onLongPress(MotionEvent e) {
View child=recycleView.findChildViewUnder(e.getX(),e.getY());
if(child!=null && clicklistener!=null){
clicklistener.onLongClick(child,recycleView.getChildAdapterPosition(child));
}
}
});
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
View child=rv.findChildViewUnder(e.getX(),e.getY());
if(child!=null && clicklistener!=null && gestureDetector.onTouchEvent(e)){
clicklistener.onClick(child,rv.getChildAdapterPosition(child));
}
return false;
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
}
ReposAdapter.java
public class ReposAdapter extends RecyclerView.Adapter<ReposAdapter.MyViewHolder> {
public ArrayList<Repo> mRepos;
public ReposAdapter(){
this.mRepos = new ArrayList<>();
}
public void setRepos(ArrayList<Repo> repos) {
mRepos = repos;
notifyDataSetChanged();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
ItemViewBinding binding;//Name of the item_view.xml in camel case + "Binding"
public MyViewHolder(ItemViewBinding b) {
super(b.getRoot());
binding = b;
}
}
@NonNull
@Override
public ReposAdapter.MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new MyViewHolder(ItemViewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(@NonNull ReposAdapter.MyViewHolder holder, int position) {
// Get the data model based on position
Repo repo = mRepos.get(position);
holder.binding.textView1.setText(repo.getName());
holder.binding.textView2.setText(repo.getDescription());
// holder.binding.textView3.setText(repo.getCreatedAt());
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy")
.withZone(ZoneId.systemDefault());
// API 26 mínima para usar parse
Instant instant = Instant.parse(repo.getCreatedAt());
String output = formatter.format( instant );
holder.binding.textView3.setText(output);
}
@Override
public int getItemCount() {
return mRepos.size();
}
}
– Retrofit
|
Cómo consumir una API y procesar la respuesta usando Retrofit |
![]() |
ApiService.java
public interface ApiService {
@GET("users/{username}/repos")
Call<ArrayList<Repo>> listRepos(@Path("username") String username);
// uso con servidor web local
@GET("ficheros/repositorios.json")
Call<ArrayList<Repo>> getRepos();
}
Creating the Retrofit instance:
ApiAdapter.java
public class ApiAdapter {
private static ApiService API_SERVICE;
public static final String BASE_URL = "https://api.github.com/";
// android:usesCleartextTraffic="true"
// public static final String BASE_URL = "http://192.168.0.10/";
public static synchronized ApiService getInstance() {
if (API_SERVICE == null) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.build();
Gson gson = new GsonBuilder()
.setDateFormat("dd-MM-yyyy")
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build();
API_SERVICE = retrofit.create(ApiService.class);
}
return API_SERVICE;
}
}
Call<ArrayList<Repo>> call = ApiAdapter.getInstance().listRepos(username);
call.enqueue(new Callback<ArrayList<Repo>>() {
@Override
public void onResponse(Call<ArrayList<Repo>> call, Response<ArrayList<Repo>> response) {
int statusCode = response.code();
User user = response.body();
}
@Override
public void onFailure(Call<ArrayList<Repo>> call, Throwable t) {
// Log error here since request failed
}
});
MainActivity.java
// https://developer.android.com/guide/topics/ui/floating-action-button
// https://material.io/components/buttons-floating-action-button/android#using-fabs
// https://material.io/resources/icons/?icon=autorenew&style=baseline
// https://stackoverflow.com/questions/25229124/format-instant-to-string
// https://stackoverflow.com/questions/19112357/java-simpledateformatyyyy-mm-ddthhmmssz-gives-timezone-as-ist
// https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html
// https://www.baeldung.com/java-datetimeformatter
public class MainActivity extends AppCompatActivity implements View.OnClickListener, Callback<ArrayList<Repo>> {
private ActivityMainBinding binding;
private ReposAdapter adapter;
private ArrayList<Repo> repos;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
binding = ActivityMainBinding.inflate(getLayoutInflater());
View view = binding.getRoot();
setContentView(view);
binding.fab.setOnClickListener(this);
adapter = new ReposAdapter();
binding.recyclerView.setAdapter(adapter);
binding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
//manage click
binding.recyclerView.addOnItemTouchListener(new RecyclerTouchListener(this, binding.recyclerView, new ClickListener() {
@Override
public void onClick(View view, final int position) {
//Values are passing to activity & to fragment as well
//Toast.makeText(getApplicationContext(), "Single Click on position :" + position, Toast.LENGTH_SHORT).show();
showMessage("Single Click on position:" + position);
//Uri uri = Uri.parse((String) repos.get(position).getUrl());
Uri uri = Uri.parse((String) repos.get(position).getHtmlUrl());
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
if (intent.resolveActivity(getPackageManager()) != null)
startActivity(intent);
else
//Toast.makeText(getApplicationContext(), "No hay un navegador", Toast.LENGTH_SHORT).show();
showMessage("No hay un navegador");
}
@Override
public void onLongClick(View view, int position) {
//Toast.makeText(getApplicationContext(), "Long press on position :" + position, Toast.LENGTH_LONG).show();
showMessage("Long press on position :" + position);
}
}));
}
//retrofit
private void showMessage(String s) {
Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
}
public void onClick(View view) {
if (view == binding.fab) {
String username = binding.editText.getText().toString();
showMessage(username);
if (username.isEmpty())
showMessage("Debe dar un nombre");
else {
// poner cuadro de progreso, si se desea
//Call<ArrayList<Repo>> call = apiService.listRepos(username);
Call<ArrayList<Repo>> call = ApiAdapter.getInstance().listRepos(username);
// uso con servidor web local
// Call<ArrayList<Repo>> call = ApiAdapter.getInstance().getRepos();
call.enqueue((Callback<ArrayList<Repo>>) this);
}
}
}
@Override
public void onResponse(Call<ArrayList<Repo>> call, Response<ArrayList<Repo>> response) {
//get the data and refresh the adapter
//and handle the wrong answer
}
@Override
public void onFailure(Call<ArrayList<Repo>> call, Throwable t) {
//handle the failure
}
}
onResponse?
@Override
public void onResponse(Call<ArrayList<Repo>> call, Response<ArrayList<Repo>> response) {
}
onFailure?
@Override
public void onFailure(Call<ArrayList<Repo>> call, Throwable t) {
}
Código:
@Override
public void onResponse(Call<ArrayList<Repo>> call, Response<ArrayList<Repo>> response) {
if (response.isSuccessful()) {
repos = response.body();
adapter.setRepos(response.body());
showMessage("Repositorios actualizados ok");
} else {
StringBuilder message = new StringBuilder();
message.append("Error en la descarga: " + response.code());
if (response.body() != null)
message.append("\n" + response.body());
if (response.errorBody() != null)
try {
message.append("\n" + response.errorBody().string());
} catch (IOException e) {
e.printStackTrace();
}
showMessage(message.toString());
}
}
@Override
public void onFailure(Call<ArrayList<Repo>> call, Throwable t) {
String message = "Fallo en la comunicación:\n";
if (t != null)
message += t.getMessage();
showMessage(message);
}
}


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