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.