Aplicación Todo en Kotlin
App en Kotlin que se comunica con el API Rest creado en laravel.alumno.me
¿Esquema de la red?
Aplicación en Kotlin
ApiService
interface ApiService {
//@POST("api/register")
@FormUrlEncoded
@POST("api/register")
suspend fun register(
@Field("name") name: String?,
@Field("email") email: String?,
@Field("password") password: String?,
@Field("confirm_password") confirmPassword: String?
): Response<RegisterResponse?>
//@POST("api/login")
@FormUrlEncoded
@POST("api/login")
suspend fun login(
@Field("email") email: String?,
@Field("password") password: String?
): Response<LoginResponse?>
}
ApiRestClient
object ApiRestClient {
private var API_SERVICE: ApiService? = null
const val BASE_URL = Configuration.BASE_URL
@get:Synchronized
val instance: ApiService?
get() {
if (API_SERVICE == null) {
val okHttpBuilder: Builder = Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
val gson = GsonBuilder()
.setDateFormat("dd-MM-yyyy")
.create()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpBuilder.build())
.build()
API_SERVICE = retrofit.create(ApiService::class.java)
}
return API_SERVICE
}
}
ApiTokenService
interface ApiTokenService {
@POST("api/logout")
//@Header("Authorization") String token
suspend fun logout(): Response<LogoutResponse?>
// @get:GET("api/tasks")
//@Header("Authorization") String token
//val tasks: Response<GetTasksResponse?>
@GET("api/tasks")
//@Header("Authorization") String token
suspend fun getTasks(): Response<GetTasksResponse?>
@POST("api/tasks")
//@Header("Authorization") String token,
suspend fun createTask(
@Body task: Task?
): Response<AddResponse?>
@PUT("api/tasks/{id}")
//@Header("Authorization") String token,
suspend fun updateTask(
@Body task: Task?,
@Path("id") id: Long
): Response<AddResponse?>
@DELETE("api/tasks/{id}")
//@Header("Authorization") String token,
suspend fun deleteTask(
@Path("id") id: Long?
): Response<DeleteResponse?>
@POST("api/email")
suspend fun sendEmail(@Body email: Email?): Response<EmailResponse?>
}
ApiTokenRestClient
object ApiTokenRestClient {
private var API_SERVICE: ApiTokenService? = null
const val BASE_URL = Configuration.BASE_URL
@Synchronized
fun getInstance(token: String?): ApiTokenService? {
if (API_SERVICE == null) {
val interceptor = Interceptor { chain ->
val newRequest = chain.request().newBuilder()
.addHeader("Accept", "application/json")
.addHeader("authorization", "Bearer $token")
.build()
chain.proceed(newRequest)
}
val okHttpBuilder: Builder = Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(5, TimeUnit.SECONDS)
.addInterceptor(interceptor)
val gson = GsonBuilder()
.setDateFormat("dd-MM-yyyy")
.create()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpBuilder.build())
.build()
API_SERVICE = retrofit.create(ApiTokenService::class.java)
}
return API_SERVICE
}
fun deleteInstance() {
API_SERVICE = null
}
}
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:id="@+id/linearLayout3"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="104dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="100dp"
android:layout_marginBottom="65dp"
android:src="@drawable/tasks_image"
app:layout_constraintBottom_toTopOf="@+id/email"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.611"
app:layout_constraintStart_toStartOf="@+id/textViewUpdate"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="7dp"
android:text="Email"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintEnd_toStartOf="@+id/email"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/email" />
<EditText
android:id="@+id/email"
android:layout_width="315dp"
android:layout_height="51dp"
android:layout_marginEnd="11dp"
android:layout_marginBottom="24dp"
android:ems="10"
android:inputType="textEmailAddress"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintBottom_toTopOf="@+id/password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textViewUpdate"
app:layout_constraintTop_toBottomOf="@+id/imageView"
android:text="paco.portada@protonmail.com" />
<TextView
android:id="@+id/textViewUpdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:text="Password"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintBottom_toBottomOf="@+id/password"
app:layout_constraintEnd_toStartOf="@+id/password"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="@+id/password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:layout_marginBottom="94dp"
android:ems="10"
android:inputType="textPassword"
android:text="123456"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintBottom_toTopOf="@+id/login"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textViewUpdate"
app:layout_constraintTop_toBottomOf="@+id/email" />
<Button
android:id="@+id/login"
android:layout_width="319dp"
android:layout_height="57dp"
android:layout_marginStart="51dp"
android:layout_marginTop="48dp"
android:layout_marginEnd="51dp"
android:background="@color/design_default_color_primary_dark"
android:padding="12dp"
android:text="Login"
android:textColor="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password" />
<Button
android:id="@+id/register"
android:layout_width="193dp"
android:layout_height="53dp"
android:layout_marginBottom="172dp"
android:background="@color/colorAccent"
android:padding="12dp"
android:text="Register"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.541"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/login"
app:layout_constraintVertical_bias="0.756" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
class MainActivity : AppCompatActivity(), View.OnClickListener {
lateinit var preferences: SharedPreferencesManager
private lateinit var progressDialog: ProgressDialog
private lateinit var binding: ActivityMainBinding
companion object {
const val APP = "ToDo App"
const val EMAIL = "email"
const val PASSWORD = "password"
const val TOKEN = "token"
private const val TAG = "LoginActivity"
private const val REQUEST_REGISTER = 1
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//activity_main.xml -> ActivityMainBinding
binding = ActivityMainBinding.inflate(layoutInflater)
val view: View = binding!!.root
setContentView(view)
preferences = SharedPreferencesManager(this)
binding!!.email.setText(preferences!!.email)
binding!!.password.setText(preferences!!.password)
binding!!.login.setOnClickListener(this)
binding!!.register.setOnClickListener(this)
}
override fun onClick(v: View) {
hideSoftKeyboard()
if (v === binding!!.login) {
if (validate() == false) {
showMessage("Error al validar los datos")
} else {
loginByServer()
}
} else if (v === binding!!.register) {
// Start the Register activity
val intent = Intent(this, RegisterActivity::class.java)
startActivityForResult(intent, REQUEST_REGISTER)
//finish();
//overridePendingTransition(R.anim.push_left_in, R.anim.push_left_out);
}
}
private fun loginByServer() {
progressDialog = ProgressDialog(this)
progressDialog!!.isIndeterminate = true
progressDialog!!.setMessage("Login ...")
progressDialog!!.setCancelable(false)
progressDialog!!.show()
binding!!.login.isEnabled = false
val email = binding!!.email.text.toString()
val password = binding!!.password.text.toString()
lifecycleScope.launch {
try {
val response = ApiRestClient.instance!!.login(email, password)
launch(Dispatchers.Main) {
progressDialog!!.dismiss()
hideSoftKeyboard()
binding!!.login.isEnabled = true
if (response.isSuccessful) {
val loginResponse = response.body()
if (loginResponse!!.success!!) {
Log.d("onResponse", "" + response.body().toString())
//showMessage(response.body().getToken());
//guardar token en shared preferences
preferences.save(
binding.email.text.toString(),
binding.password.text.toString(),
loginResponse!!.data!!.token
)
startActivity(Intent(applicationContext, PanelActivity::class.java))
finish()
} else {
showMessage("Error in login: Email/Password incorrectos")
showMessage(loginResponse!!.message)
Log.d("Login error", loginResponse!!.message.toString())
}
} else {
val message = StringBuilder()
message.append("Failure in login: ")
if (response.body() != null) {
val loginResponse = response.body()
message.append("\n" + loginResponse!!.message);
}
if (response.errorBody() != null)
try {
message.append("\n" + response.errorBody()!!.toString())
} catch (e: IOException) {
e.printStackTrace()
}
showMessage(message.toString())
}
}
} catch (e: Exception) {
launch(Dispatchers.Main) {
hideSoftKeyboard()
progressDialog!!.dismiss()
binding!!.login.isEnabled = true
binding!!.register.isEnabled = true
var message: String = "Failure in the communication\n"
if (e != null) {
Log.d("onFailure", e.message.toString())
message += e.message.toString()
}
showMessage(message)
}
}
}
}
private fun showMessage(s: String?) {
Toast.makeText(this, s, Toast.LENGTH_SHORT).show()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_REGISTER) {
if (resultCode == RESULT_OK) {
// TODO: Implement successful signup logic here
// Por defecto se hace login automáticamente después del registro
// Habría que validar el email antes de realizar login
//Guardar token y lanzar Panel
preferences!!.save(
data!!.extras!!.getString("email"),
data.extras!!.getString("password"),
data.extras!!.getString("token")
)
startActivity(Intent(this, PanelActivity::class.java))
finish()
//binding.inputEmail.setText(data.getExtras().getString("email"));
//binding.inputPassword.setText(data.getExtras().getString("password"));
} else if (requestCode == RESULT_CANCELED) {
//no hacer nada, volver al login
showMessage("Registro cancelado")
}
}
}
fun validate(): Boolean {
var valid = true
val email = binding!!.email.text.toString()
val password = binding!!.password.text.toString()
if (email.isEmpty() || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
binding!!.email.error = "Enter a valid email address"
requestFocus(binding!!.email)
valid = false
} else {
binding!!.email.error = null
}
if (password.isEmpty()) {
binding!!.password.error = "Password is empty"
requestFocus(binding!!.password)
valid = false
} else {
binding!!.password.error = null
}
return valid
}
private fun requestFocus(view: View) {
if (view.requestFocus()) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
}
}
fun hideSoftKeyboard() {
if (currentFocus != null) {
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(currentFocus!!.windowToken, 0)
}
}
}
activity_panel.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"
android:id="@+id/ConstraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.circularreveal.coordinatorlayout.CircularRevealCoordinatorLayout
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_toTopOf="parent">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="Add task"
android:layout_gravity="bottom|right"
android:layout_marginRight="30sp"
android:layout_marginBottom="30sp"
android:clickable="true"
app:backgroundTint="@color/colorDefault"
app:srcCompat="@drawable/pen_plus" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:backgroundTint="@color/design_default_color_primary" />
</com.google.android.material.circularreveal.coordinatorlayout.CircularRevealCoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
PanelActivity
class PanelActivity : AppCompatActivity(), View.OnClickListener {
var positionClicked = 0
lateinit var progressDialog: ProgressDialog
companion object {
const val ADD_CODE = 100
const val UPDATE_CODE = 200
const val OK = 1
}
//ApiService apiService;
lateinit var preferences: SharedPreferencesManager
private lateinit var binding: ActivityPanelBinding
private lateinit var adapter: TodoAdapter
// private val listTasks = mutableListOf<Task>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//activity_main.xml -> ActivityMainBinding
binding = ActivityPanelBinding.inflate(layoutInflater)
val view: View = binding!!.root
setContentView(view)
binding!!.floatingActionButton.setOnClickListener(this)
preferences = SharedPreferencesManager(this)
//showMessage("panel: " + preferences.getToken());
initRecyclerView();
//Destruir la instancia de Retrofit para que se cree una con el nuevo token
ApiTokenRestClient.deleteInstance()
downloadTasks()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val menuInflater = menuInflater
menuInflater.inflate(R.menu.menu_toolbar, menu)
// super.onCreateOptionsMenu(menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.refresh ->
//petición al servidor para descargar de nuevo los sitios
downloadTasks()
R.id.email -> {
//send an email
val i = Intent(this, EmailActivity::class.java)
startActivity(i)
}
R.id.exit -> {
//petición al servidor para anular el token (a la ruta /api/logout)
logout()
preferences!!.saveToken(null, null)
startActivity(Intent(applicationContext, MainActivity::class.java))
finish()
}
}
return true
}
private fun initRecyclerView() {
adapter = TodoAdapter()
// binding.rvDogs.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
binding!!.recyclerView.addOnItemTouchListener(
RecyclerTouchListener(
this,
binding!!.recyclerView,
object : ClickListener {
override fun onClick(view: View?, position: Int) {
showMessage("Single Click on task with id: " + adapter!!.getAt(position).id)
modify(adapter!!.getAt(position))
positionClicked = position
}
override fun onLongClick(view: View?, position: Int) {
showMessage("Long press on position :$position")
}
})
)
val itemSwipe = object : SimpleCallback(0, RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
if (binding.recyclerView.isEnabled) {
val position = viewHolder.adapterPosition
val id = adapter?.getId(position)
confirmDialog(id, position)
} else {
binding.recyclerView.adapter?.notifyItemChanged(viewHolder.position)
}
}
}
val swap = ItemTouchHelper(itemSwipe)
swap.attachToRecyclerView(binding.recyclerView)
}
override fun onClick(v: View) {
if (v === binding!!.floatingActionButton) {
val i = Intent(this, AddActivity::class.java)
startActivityForResult(i, ADD_CODE)
}
}
private fun downloadTasks() {
progressDialog = ProgressDialog(this)
progressDialog!!.setProgressStyle(ProgressDialog.STYLE_SPINNER)
progressDialog!!.setMessage("Connecting . . .")
progressDialog!!.setCancelable(false)
progressDialog!!.show()
lifecycleScope.launch {
try {
val response = ApiTokenRestClient.getInstance(preferences!!.token)!!.getTasks()
launch(Dispatchers.Main) {
progressDialog!!.dismiss()
if (response.isSuccessful) {
val getTasksResponse = response.body()
if (getTasksResponse!!.success!!) {
adapter!!.setTasks(getTasksResponse.data!!)
showMessage("Tasks downloaded ok")
} else {
showMessage("Error downloading the tasks: " + getTasksResponse.message)
}
} else {
val message = StringBuilder()
message.append("Error downloading the tasks: ")
if (response.body() != null)
message.append("\n" + response.body()!!.toString());
if (response.errorBody() != null)
try {
message.append("\n" + response.errorBody()!!.toString())
} catch (e: IOException) {
e.printStackTrace()
}
showMessage(message.toString())
}
}
} catch (e: Exception) {
launch(Dispatchers.Main) {
// hideSoftKeyboard()
// progressDialog!!.dismiss()
var message: String = "Failure in the communication\n"
if (e != null) {
Log.d("onFailure", e.message.toString())
message += e.message.toString()
}
showMessage(message)
}
}
}
}
private fun showMessage(s: String?) {
Toast.makeText(this, s, Toast.LENGTH_SHORT).show()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val task = Task()
if (requestCode == ADD_CODE) if (resultCode == OK) {
task.id = data!!.getIntExtra("id", 1).toLong()
task.description = data.getStringExtra("description")
task.createdAt = data.getStringExtra("createdAt")
adapter!!.add(task)
}
if (requestCode == UPDATE_CODE) if (resultCode == OK) {
task.id = data!!.getIntExtra("id", 1).toLong()
task.description = data.getStringExtra("description")
task.createdAt = data.getStringExtra("createdAt")
adapter!!.modifyAt(task, positionClicked)
}
}
private fun modify(task: Task) {
var i = Intent(this, UpdateActivity::class.java)
i.putExtra("task", task)
startActivityForResult(i, UPDATE_CODE)
}
// private fun confirmDialog(idTask: Int, description: String, position: Int) {
private fun confirmDialog(id: Long?, position: Int) {
val builder = AlertDialog.Builder(this)
val description = adapter.getAt(position).description
builder.setMessage("$description\n Do you want to delete?")
.setTitle("Delete")
.setPositiveButton("Confirm") { dialog, which ->
connection(id, position)
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, which ->
binding.recyclerView.adapter?.notifyItemChanged(position)
}
builder.show()
}
private fun connection(id: Long?, position: Int) {
progressDialog!!.setProgressStyle(ProgressDialog.STYLE_SPINNER)
progressDialog!!.setMessage("Connecting . . .")
progressDialog!!.setCancelable(false)
progressDialog!!.show()
lifecycleScope.launch {
try {
val response = ApiTokenRestClient.getInstance(preferences!!.token)!!.deleteTask(id)
launch(Dispatchers.Main) {
// hideSoftKeyboard()
progressDialog!!.dismiss()
if (response.isSuccessful) {
val deleteResponse = response.body()
if (deleteResponse!!.success!!) {
adapter!!.removeAt(position)
showMessage("Task deleted OK")
} else
showMessage("Error deleting the task")
} else {
val message = StringBuilder()
message.append("Error downloading the tasks: ")
if (response.body() != null)
message.append("\n" + response.body()!!.toString());
if (response.errorBody() != null)
try {
message.append("\n" + response.errorBody()!!.toString())
} catch (e: IOException) {
e.printStackTrace()
}
showMessage(message.toString())
}
}
} catch (e: Exception) {
launch(Dispatchers.Main) {
// hideSoftKeyboard()
progressDialog!!.dismiss()
var message: String = "Failure in the communication\n"
if (e != null) {
Log.d("onFailure", e.message.toString())
message += e.message.toString()
}
showMessage(message)
}
}
}
}
private fun logout() {
lifecycleScope.launch {
try {
val response = ApiTokenRestClient.getInstance(preferences!!.token)!!.logout()
launch(Dispatchers.Main) {
progressDialog!!.dismiss()
if (response.isSuccessful) {
val logoutResponse = response.body()
if (logoutResponse!!.success!!) {
showMessage("Logout OK")
} else showMessage("Error in logout")
} else {
val message = StringBuilder()
message.append("Error in logout: ")
if (response.body() != null)
message.append("\n" + response.body()!!.toString());
if (response.errorBody() != null)
try {
message.append("\n" + response.errorBody()!!.toString())
} catch (e: IOException) {
e.printStackTrace()
}
showMessage(message.toString())
}
}
} catch (e: Exception) {
launch(Dispatchers.Main) {
// hideSoftKeyboard()
// progressDialog!!.dismiss()
var message: String = "Failure in the communication\n"
if (e != null) {
Log.d("onFailure", e.message.toString())
message += e.message.toString()
}
showMessage(message)
}
}
}
}
}




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