We are back for a second post to show you the progress of our Android application.
To read the first article, follow the guide.
The objective of our application is to centralize all Lunatech internal services. We will talk about three topics discovered during the development of our application. We start by explaining how to integrate a secure connection with Google and Firebase. Then we see the process of creating our main screen and finally how to call an external API efficiently.
Authentication with Google and Firebase
First of all, if you are looking for a tutorial step by step, I invite you to look at this link which explains in detail how to implement the solution, that will follow, in your code. In this blog post, we explore the main steps to set up an authentication with Google and Firebase for an Android application. Our workflow to login an user goes through four major phases:
1. Client creation
First of all, we need to create a client that will allow us to authenticate later. In this part of the code, we can specify all the information we want to retrieve from each Google Account. In this case, we only need the user’s email. A good practice is to limit fields to be retrieved to the strict minimum in order to avoid taking risks with unnecessary personal information.
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.GoogleSignInOptions(getString(R.string.client_id)) // necessary for Firebase
.requestEmail() // take only user's email
.build()
mGoogleSignInClient = GoogleSignIn.getClient(this, gso)
auth = FirebaseAuth.getInstance()
2. Login to Google
Then, we set up a function and we link it to the login button.
This function starts the connection process from our client.
After that, the startActivityForResult
function will display the Google login page.
Finally, the onActivityResult
function will be called when the user logs in with his Google Account.
This function will provide us with a GoogleSignInAccount
object with important information for the future.
private fun signIn() {
val signInIntent = mGoogleSignInClient.signInIntent
startActivityForResult(signInIntent, RC_SIGN_IN) // display Google Login page
}
3. Verification of the user’s identity
Now, we check if the user belongs to the company domain. If this is the case, we proceed to the next step with Firebase, otherwise we display an error message. At this point, the connection with Google is established and all we have to do is link the user to Firebase.
if(authorizedDomain.contains(domain)) firebaseAuthWithGoogle(account) else updateUI()
4. Login to Firebase
For the last step, we connect the user to Firebase.
Once again, we use the power of Kotlin to handle null
in an efficient way.
After the connection is established with both parties, we update the user interface.
Thanks to the variable auth
, we will be able to find our user later and implement a verification when the application is opened (onStart
function).
This method will allow users to avoid having to log in each time they use the application.
private fun firebaseAuthWithGoogle(account: GoogleSignInAccount?) {
val credential = GoogleAuthProvider.getCredential(account?.idToken ?: "null", null)
auth.signInWithCredential(credential)
.addOnCompleteListener(this) { task ->
val userOrNull = if(task.isSuccessful) auth.currentUser else null
updateUI(userOrNull)
}
}
Figure 1 illustrates all the steps of the connection workflow with a sequence diagram.
Figure 2 shows the current login screen with the Google button to launch the login workflow explained above. After logging in, we are on the main screen. We will explain how this view is organized to make it maintainable and customizable.
Main screen
We decided to organize the main screen as a list of Card
.
Each Card
corresponds to a different service (i.e. Figure 3: Weather, Annia, etc.).
We want to have the most flexible interface possible to increase or reduce the number of services available afterwards.
In addition, we have added a drawer from the left side of the screen, containing the list of applications and user actions as logout
.
To obtain this flexible result, we use RecyclerView
coupled with CardView
.
I recommend this tutorial to implement, step by step, the same kind of interface.
In Figure 3, you can see the final result.
A RecyclerView
is a view adapted to this use case.
It allows to implement a view corresponding to an item
, here, a Card
.
Then, an adapter is defined.
Finally, the RecyclerView and its adapter will take care of creating all the sub-views and make several optimizations to make the application more fluid.
We will see the code corresponding to the main steps of this implementation.
Our main screen includes three RecycleView
.
The first one is used for the list displayed on the main screen.
The other two make up the sliding drawer: for the favorite applications and for the other applications.
To reduce the code to be displayed, we will explore the RecycleView used for the main screen.
1. Description of the data
The first step is to define the class representing the dataset for the adapter.
This class includes three attributes to describe each of our applications and a clickListener
function to adapt the action followed by clicking on each service.
The id
attribute allows you to optimize the processing of views thereafter.
I used several tips to optimize RecyclerView like this id
attribute.
If you are interested, I can make a dedicated article so don’t hesitate to let me know!
data class ModelApplication(val icon: Int, val name: String, val description: String, var like: Boolean = false, val id: Long = idGenerator++, val clickListener: (ModelApplication) -> Unit) {
companion object { var idGenerator: Long = 0 }
}
2. Adaptater creation
Afterwards, we implement the adapter to transform instances of the previous class into a view (Card
in this case).
We start by selecting fields to update thanks to their id
.
We update all these fields with our ModelApplication instance.
Finally, we link a clickListener on the heart icon to have the like interaction to keep applications we use most regularly in focus.
You can see the functionality of Like in Figure 4.
We link another clickListener on our Card
to trigger the interaction specific to each application.
As we can see, apart from the clickListener to like an application, all the other information is filled in our ModelApplication instance.
Therefore, we only need to create a new ModelApplication instance in our dataset and our logic will take care of creating all views.
class ModelViewHolder(itemV: View, val context: Context) : RecyclerView.ViewHolder(itemV) {
// We select all fields that need to be updated
private val nameApp = itemV.name_card
private val descriptionApp = itemV.description_card
private val iconApp = itemV.icon_app
private val iconLove = itemV.icon_love
private val layoutCard = itemV.layout_card
fun bindItems(application: ModelApplication, clickListener: (ModelApplication) -> Unit) {
// We set fields in function of the ModelApplication instance
nameApp.text = application.name
descriptionApp.text = application.description
iconApp.setImageDrawable(ContextCompat.getDrawable(context, application.icon))
val drawableLove = if (application.like) {
layoutCard.setBackgroundResource(R.drawable.card_edge)
R.drawable.ic_favorite_black_24dp
}
else {
layoutCard.setBackgroundResource(0)
R.drawable.ic_favorite_border_black_24dp
}
iconLove.setImageDrawable(ContextCompat.getDrawable(context, drawableLove))
iconLove.setOnClickListener { clickListener(application) }
layoutCard.setOnClickListener { application.clickListener(application) }
}
}
3. Assembly of all elements
The last step is to gather the above elements.
We start by defining a viewManager
that represent the organization of all our sub-elements.
We choose a LinearLayoutManager
which, by default, organizes our child views vertically, perfect for this use case!
Then, we define the adapter with the dataset and the clickListener associated with the functionality like.
That’s all for the core of the code!
If we want to add or remove a service, we just have to modify our dataset (application
variable here) and everything else is automated.
val viewManager = LinearLayoutManager(this)
val mainScreenAdapter = RecyclerAdapterMainScreen(applications) { application: ModelApplication -> clickListenerLike(application) }
mainScreenAdapter.setHasStableIds(true) // Optimization with the id
val recyclerView = findViewById(R.id.recycler_view)
listRecyclerView.apply {
setHasFixedSize(true) // Other optimisation
layoutManager = viewManager
adapter = mainScreenAdapter
}
Use external APIs with Retrofit
We move on to the last part of this second blog post: using an external API from our Android application.
To do this, we have chosen to use the HTTP client Retrofit.
We chose Retrofit
because it integrates perfectly with Android applications and allows you to make asynchronous calls.
For this example, we decided to implement a very simple service to retrieve the current weather in order to be able to perform our tests in a small environment.
To do this, we use OpenWeatherMap.
We break down the problem into several steps:
1. Description of API calls
We create an interface to describe the interaction with the API.
It’s not necessary to write the entire URL because we will specify, when creating the HTTP client, a base URL to concatenate with start of each request.
So we have a GET
request taking two parameters and returning an instance of Call<WeatherResponse>
.
The parameter q
represents the name of the city and APPID
our API key.
Disclaimer: For our first experiments, we stored our API key in the Android application and this is a very bad practice to avoid if you don’t want to have bad surprises.
interface WeatherService {
@GET("/data/2.5/weather?units=metric")
fun getCurrentWeatherData(@Query("q") cityName: String, @Query("APPID") app_id: String): Call<WeatherResponse>
}
2. HTTP client creation
Then we instantiated the HTTP client.
We configure a base URL that will be concatenated at the beginning of each request.
We add a GsonConverter
to transform the response of each request directly into an instance of an object that we will see next.
retrofit = Retrofit.Builder()
.baseUrl(getString(BaseUrl)) // BaseUrl = http://api.openweathermap.org
.addConverterFactory(GsonConverterFactory.create())
.build()
service = retrofit.create(WeatherService::class.java)
3. Handle responses returned by the API
To continue, we define a class allowing the GsonConverter
to know how to transform responses.
To do this, you must first look at the fields returned by the API.
OpenWeatherMap provides us a description of a standard answer and an explanation of each field.
In our case, we are interested by the description of the weather (cloudy, sunny, etc.), the image corresponding to the weather and the current temperature.
class WeatherResponse {
@SerializedName("weather")
var weather = ArrayList<Weather>()
@SerializedName("main")
var main: Main = Main()
}
class Weather {
@SerializedName("description")
var description: String = "?" // overcast, fog, etc.
@SerializedName("icon")
var icon: String = "11d" // name of the weather image
}
class Main {
@SerializedName("temp")
var temp: Float = 0.toFloat()
}
4. Calls the API
Finally, we can now make calls to the API asynchronously. If the request fails, we display an error message and redirect the user to the main page. If the request is successful, we display results on the user interface (Picasso is a library that allows you to optimally manage remote image download).
val call = service.getCurrentWeatherData(city, getString(AppId))
// Start API call
call.enqueue(object : Callback<WeatherResponse> {
override fun onResponse(call: Call<WeatherResponse>, response: Response<WeatherResponse>) {
if (response.code() == 200) {
val imageViewWeather: ImageView = findViewById(R.id.icon_weather)
val weatherResponse: WeatherResponse = response.body() ?: WeatherResponse()
val weatherObj = weatherResponse.weather.getOrElse(0) { Weather() }
Picasso.with(applicationContext)
.load("${getString(BaseUrl)}/img/w/${weatherObj.icon}.png")
.into(imageViewWeather)
temperature.text = getString(R.string.symbole_celcius, weatherResponse.main.temp.toString())
description.text = weatherObj.description.capitalize()
cityTextView.setText(city)
}
else { handleErrorWeather() }
}
override fun onFailure(call: Call<WeatherResponse>, t: Throwable) {
handleErrorWeather(t.message)
}
})
As you can see, Retrofit is a perfect client for using external APIs in Android applications. Figure 5 shows the final result of the weather service.
We finished for this well-filled blog post, thank you for reading!
If you have any comments or questions, please do not hesitate to contact me. We’ll meet again next time to look at the progress of this project together!