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:

Documentation

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

Extensión JSON para Firefox

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:

Codepath: FAB

Android developer

Material.io: Using fabs

Icon autorenew

activity_main.xml
<?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

Using the RecyclerView

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

Consuming APIs with Retrofit

Cómo consumir una API y procesar la respuesta usando Retrofit

Customize Network Timeouts

Patrón Singleton

Define the endpoints:

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;
    }
}

Accesing the API

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