Android Programming 301: Advanced Features
Gradle
Gradle is a build that works with Android. It provides many advantages over Android. Therefore, it is very important for Android developers to learn the Gradle structure.
It is primarily used in the compilation phase. It notifies the developer of errors and deficiencies in the project. In this way, a faster development process can be realized.
In addition, different libraries and packages are downloaded and managed within the project, enabling a more up-to-date, efficient and modern application to be prepared.
- local.properties: It is the file where needed structures such as SDK and file paths are kept.
- settings.gradle: It keeps reference information of all modules that make up the project. The main project is the file that defines what the subprojects are, how to name the scripts.
- gradle.properties: It is for specifying the architectural needs of the project while compiling it.
- gradle-wrapper.properties: This is the file in which the gradle version is checked and will be replaced with “distributionUrl” when it is wanted to be changed.
- build.gradle(Module:app): All settings to be used in the module are made from this section. It consists of two main parts as “android{…}” and “dependencies{…}”. There are operations such as basic version settings related to the project in Android. In Dependencies, the necessary dependencies used in the project are given with “implementation”.
- build.gradle(Project:…): It is the top level gradle file. Control of basically used structures such as Gradle version, Kotlin version is done in this class.
Storage Operations
A lot of data is kept on the device in order to be faster and more efficient without disrupting the user’s work. For example, an “offline” feature can be brought to a device using the internet.
SharedPreferences
SharedPreferences allows our data to be kept on the application in android operating system management. This feature does not provide large data retention. It is used to keep small information such as settings.
When wanting to write a value to a SharedPreferences object;
fun writeSP() {
val pref = getSharedPreferences(packageName, MODE_PRIVATE)
val editor = pref.edit()
editor.putString("key", "SP Text")
editor.commit()
}
By creating an editor over the SharedPreferences object, values are actually assigned in accordance with the “key”, “value” structure and these are kept by commit.
If the held data wants to be read;
fun readSP(): String {
val pref = getSharedPreferences(packageName, MODE_PRIVATE)
return pref.getString("key", "Default Value")!!
}
When the assigned value is wanted to be read, a get operation is performed on the SharedPreferences object created. However, it is added that which value should be given as default when the desired “key” is not available.
This data is cleared when “data cleared” or “app uninstalled” from application settings.
Local Database
We use local databases in cases where applications need to work or data needs to be kept on the device in cases where there is no internet. By default, it works with SQLite database on Android. Therefore, in order to create a database, a class derived from SQLiteOpenHelper is required.
Instead, the ROOM library shared by Android makes SQLite operations faster and easier. Introduced as part of Android Jetpack, ROOM is recommended to be used for SQL operations.
ROOM
Room can be thought of as a layer for using SQLite for database access. In this way, it offers a simplified database path.
It provides an annotation structure that minimizes repetitive and error-prone code. Thanks to these annotation markings, an easier and more open structure can be established.
Room does not come ready-made within an android project. “implementation” codes should be written as “dependencies” in build.gradle.
Components
On the basis of Room, it consists of three main structures:
- Database Class: This structure is the class that holds the database, that is, does a main access task.
- Data Access Objects: This structure, called DAO, is the structure where queries such as adding and deleting the database tables to be used in the project are created.
- Data Entities: Tables that are the building blocks of databases are called “data entities”.
Sample App
Basically, a ROOM structure can be created using the components described as follows;
- Entity
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
The Entity object represents the table we will use in the project. The table we want to use in our project is named “User”. It contains 3 different variables.
The uid variable acts as an ID in the table. With @PrimaryKey every element in the table will have a unique value.
Other variables specified with @ColumnInfo will receive the given column name “first_name” and “last_name”, not their variable name.
- Data Access Object (DAO)
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List<User>
@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +"last_name LIKE :last LIMIT 1")
fun findByName(first: String, last: String): User
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
Classes specified as DAO and containing all database queries are marked with @Dao annotation. Now, in this class, adding, removing, updating or fetching data for Entity can be done as desired.
@Query allows us to build our own queries, user-interactively or otherwise.
The @Insert or @Delete operations, which are basic by ROOM, are made more practical with the given annotations.
- Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Finally, a class must be defined to hold the database. Thanks to this class, certain DAO and Entity classes are kept together.
The @Database annotatin structure should be used for this operation. Required Entity classes are defined in a list structure. The DAO class, in which our database queries are included, is added as a function.
Since this class is a ROOM class, RoomDatabase() needs to be extended. Thanks to the abstarct structure, it is aimed to call functions directly.
Setup
Database objects must be one. All changes should be made on these classes, no different copy should be created. Therefore, an encoding should be made in the “Instance” logic of the RoomDatabase class.
Instance structure can be built on a different class whether onCreate is desired.
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java,
"database-name")
.build()
First of all, a database class should be built by calling databaseBuilder from Room.
This function contains a “context” at the “application” level, an extended class @RoomDatabase, and the name to be given to the database.
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
And here, the DAO functions created in many operations such as user clicks, loading the screen are called and the necessary data comes in the desired format.
Internet
Internet is used for many different functional features. In this way, internet access is provided for purposes such as advertising and information display.
The following conditions must be added to the project for internet access;
- Internet Permission: With this line <uses-permission android:name=”android.permission.INTERNET” /> to be added to Android Manifest.xml file, project internet access will be allowed in the project.
- Internet Control: Another <uses-permission android:name=”android.permission.ACCESS_NETWORK_STATE” /> code line allows us to access whether there is internet access.
- Insecure Connection (http): If there is no SSL service in the internet service to be accessed, that is, a site with the extension “http” instead of “https”, the access is blocked by Android. If you are sure that you are making secure connections <uses-permission android:name=”android.permission.ACCESS_NETWORK_STATE” /> you can unblock it with this line of code.
- Web Service Connections
Web services return the request made to them in a certain format. According to this return made on the Android side, a read is performed.
- XML Web Services: Services that return data in XML file structure. The returned data is located between <?xml></xml>.
- JSON is Web Services: Json, also known as Rest services, are services that contain information between “{…}” curly brackets.
Service needs are received in JSON format in mobile programming. JSON data structure is more performant than XML because it contains fewer characters.
Retrofit
Retrofit is the most preferred library like OkHttp in network operations. Retrofit is more frequent and easier to use in mobile programming.
For Retrofit, functions that manage connections on the network are written in an interface class. These functions are called from a Retrofit object created using the Retrofit.Builder() function.
With Retrofit, an internet-based application can be performed in a simple way. For example;
- First of all, we need to create our Retrofit.Builder() class. Thanks to this class, it can be converted to the desired format by calling Retrofit functions from the desired point in the application. It should be treated as an instance. Instead of creating new objects, operations must be performed on the same Retrofit variable.
object RetrofitClient {
fun getInstance(): Retrofit { var retrofit: Retrofit = retrofit2.Retrofit.Builder()
.baseUrl("BASE_URL")
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit
}
}
Retrofit object is started to be created via Builder. With BaseUrl, the internet service desired to be accessed is reached. Necessary information is called upon request through this service. Thanks to ConvertorFactory, data in Gson format is converted to readable format. In the last part, the variable is created with “build”.
- The data in JSON format that we will read is modeled with the “data” class. The variable name written in @SerializedName is the corresponding field in JSON format.
data class User(
@SerializedName("id")
var id: Int,
@SerializedName("first_name")
var firstName: String,
@SerializedName("last_name")
var lastName: String
)
- Thanks to the “UserInterface” class created, we perform API requests to be used in the project as a function. Thanks to the “@GET” method, data is received from the returned JSON source. This method takes the necessary JSON file from the service by adding the BASE_URL to the URL it contains. The function should give our data class, which we defined as the “return” value.
interface UserInterface{
@GET("/api/users")
suspend fun getAllUsers(): Response<User>
}
- While creating the function from which we receive our data, the Retrofit variable is first created via Instance. By connecting the Interface class to this variable, the desired function is called. The variable value we include in Response allows us to control “isSuccessful()”. All data has been successfully retrieved from the service thanks to the called function. If there is a service or data related situation, the user is informed in the “else” block.
fun getAllUserList() {
var retrofit = RetrofitClient.getInstance()
var userInterface = retrofit.create(UserInterface::class.java)
lifecycleScope.launchWhenCreated {
try {
val response = userInterface.getAllUsers()
if (response.isSuccessful()) {
// Success Code
} else {
Toast.makeText(
this@MainActivity,
response.errorBody().toString(),
Toast.LENGTH_LONG
).show()
}
}catch (Ex:Exception){
Log.e("Error",Ex.localizedMessage)
}
}
}
In this way, a step has been taken to the advanced and very important subject of Android application development.
Advanced Location Operations
Location services are an important feature used in many projects and applications. As a standard, the location service is activated by obtaining the necessary permissions from the user.
The location is not taken directly in the application. This information is reported to the operating system by the application and the action is taken according to the response. LocationManager is the basic location function used.
- ACCESS_COARSE_LOCATION: These are the permissions required to get locations close to the user’s location.
- ACCESS_FINE_LOCATION: Required to get the exact location of the user.
- ACCESS_MEDIA_LOCATION: It is the permission to obtain location information over the user’s share.
- ACCESS_BACKGROUND_LOCATION: The permission required to access the user’s background location information. COARSE and FINE location permissions are also needed for this.
In order to perform location operations, a variable created via LocationListener is needed first. In this function, the necessary functions are implemented and the location information is reached.
val locationListener: LocationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {}
override fun onStatusChanged(provider: String, status: Int, eztras: Bundle) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
In the onLocationChanged(…){…} method, when you want to get the location information once and stop the location data;
locationManager!!.removeUpdates(this)
The location update via the LocationManager is received with the requestLocationUpdates() method. The LocationListener variable is given to this function.
- Provider: Location information is obtained not only from GPS but also from Network. In this section, it is given where to get location information.
- minTimeMs: It is the time that indicates how many milliseconds the data will be received most frequently. 1000 milliseconds is given in our example method. It means that data will be updated every second.
- minDistanceM: It is stated that at least how much of a position change we will be aware of. It is given in meters. When there is a change more than the specified distance, the location information is transmitted to the application. This value is very sensitive. Giving too low values will cause unnecessary location information to be taken and the performance of the device to be negatively affected.
- listener: LocationManager takes action by notifying the locationListener object at every change.
locationManager.requestLocationUpdates() {
LocationManager.GPS_PROVIDER,
1000,
0.001f,
locationListener
}
Google Maps
Thanks to Google maps, maps can be easily activated in the application. In this way, the tracking or location image on the map interacts with the user.
In the project, MapsActivity is included in the project via Activity and the environment is prepared with a ready-made maps template. In order to access the map services, the API Key is obtained through the Google account and the services are started to be used.
You can find the official map document HERE.
Advanced Features
Services
Services are structures that can do work on their own in the background, independent of Activity structures. Classes that can do background work should derive from the “Service” class.
- Unbounded Service: It is started by using the startService method via Intent. These services cannot be controlled unless additional methods are developed. There is no control mechanism as standard.
- Bounded Service: Services that are started through the object using the bindService method. The states, methods or variables of the service can be accessed.
💡 Bounded and Unbounded services have their own different lifecycles. They perform their functions within the project in accordance with these life cycles.
Services Lifecycle
Unbounded Service: Unbounded Services are created with onCreate() and terminated by calling the onDestroy() method after its execution. Considering its life cycle, it does not have any external control.
Bounded Service: Created with onCreate(). The onDestroy() method is called after the interventions made in the onBind() and onUnBind() methods.
You can find more detailed official information about the services HERE.
Broadcast Receiver
Android operating system includes broadcast logic. The Broadcast Receiver allows us to capture broadcasts within the app. They are structures that broadcast to all applications for certain functions in the Android operating system (eg ‘Message received, Search started, Location changed’ etc.). It does not have any visual structure. They are processes that will take place in the background, such as service structures.
In an Android project, a Brodcast Receiver class can be created via Other > Broadcast Receiver by right-clicking on the package.
- onReceive: Gerekli yayınlar yakalandığında bu metot içerisinde yer alan işlemler çağrılmaktadır.
In an Android project, a Brodcast Receiver class can be created via Other > Broadcast Receiver by right-clicking on the package.
Örneğin;
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="BRODCAST_NAME"/>
</intent-filter>
</receiver>
It is necessary to define “receiver” in the activity. Because the operating system should know that the operation will be performed with that receiver.
val filter = IntentFilter("BROADCAST_NAME")
val receiver = MyReceiver()
registerReceiver(receiver, filter)
Creating a Broadcast Receiver
For example, when an SMS arrives, we can call the BrodcastRecevicer object for this broadcast;
- First of all, we create the BrodcastReceiver class. We need to receive the incoming SMS notification inside the onReceiver method. From this message we can get the text written in the message or the sender number.
class MyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.extras != null) {
(intent.extras!!["pdus"] as Array<Any>?)?.let {
var messageText = ""
var sms: SmsMessage? = null
for (pdu in it) {
sms = SmsMessage.createFromPdu(pdu as ByteArray)
messageText += sms.messageBody
}
val sender: String? = sms!!.originatingAddress
Log.d("TAG","$messageText $sms $sender")
}
}
}
}
- Then we need to define in AndroidManifest.xml. The field added to the Intent Filter should be the broadcast name required for SMS reading. In addition, necessary permissions must be obtained to read SMS in <uses-permission>.
<manifest ...>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<application
...>
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
...
</activity>
</application>
</manifest>
- This BrodcastReceiver object must be defined on the Activity. In this way, the SMS sent to the device will appear on the LogCat screen as a LOG message.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val filter = IntentFilter("android.provider.Telephony.SMS_RECEIVED")
val receiver = MyReceiver()
registerReceiver(receiver, filter)
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.RECEIVE_SMS, android.Manifest.permission.READ_SMS), 1)
}
}
Broadcasting a BrodcastReceiver
We are given the opportunity to create and capture our own publications within a project. In this way, a broadcast operation can also be performed within the application. For example, when user information is updated, it is updated on other applications as well.
For example; By creating a certain Intent “Action” within the project, broadcasts sent with the given name can be received.
- In the created BroadcastReceiver class, the value sent to the intent value with the keyword “value” is received and the message is printed on the Log screen.
class SpecialReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val str = intent.getStringExtra("value")
Log.i("SpecialReceiver", "Value: $str")
}
}
- This publication, which we have specially created in AndroidManifest.xml, is not confused with different recipients by using the domain address.
<receiver
android:name=".SpecialReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.ibrahimcanerdogan.android301.specialbrodcast"/>
</intent-filter>
</receiver>
- This BrodcastReceiver created on the Activity must be defined. Afterwards, an Intent object should be created with the name specified in the <action> in the Receiver class.
val filter = IntentFilter("com.ibrahimcanerdogan.specialbrodcast")
val receiver = SpecialReceiver()
registerReceiver(receiver, filter)
val i = Intent("com.ibrahimcanerdogan.android301.specialbrodcast")
i.putExtra("value", "İbrahim Can Erdoğan")
sendBroadcast(i)
When the application is run, the value will be printed on the Log screen.
Alarm Manager
In applications, it may be necessary to define processes that need to run at a certain time or from time to time. Alarm Manager works in the background to manage these processes.
- Real Time Clock: A time is set and triggering is expected at that time. There are two different types of time-oriented Alarm Manager structures.
RTC: If the device is in sleep state, it does not work, it waits for the device to turn on.
RTC_WAKEUP: If the device is in sleep state, it starts the device and thus the alarm is triggered.
- By Elapsed Time: A time interval is defined, not a fixed time. For example, it is used for needs such as working every half hour, every day. There are two different types of time-oriented Alarm Manager structures.
ELAPSED_REALTIME: It does not wake the device. The alarm is then triggered.
ELAPSED_REALTIME_WAKEUP: It wakes up the device and fulfills the requirement of the alarm.
The Alarm Manager structure needs 3 basic stages to work.
- Determination of time and type of time
- Setting up an alarm
- Creating a Receiver to perform alarm operations
Creating an Alarm
In this example, the application that runs when a certain time is selected is programmed.
- First of all, it is necessary to give the necessary permissions to AndroidManifest.xml.
<uses-permission android:name="android.permission.WAKE_LOCK"/>
- An alarm sound has been added with the RingtoneManager on a created Activity. Then, when the button on the relevant .xml is clicked, the alarm is turned off.
class AlarmActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_alarm)
val buttonClose = findViewById<Button>(R.id.button2)
var ringtoneURI = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
if (ringtoneURI == null) ringtoneURI = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val ringtone = RingtoneManager.getRingtone(this, ringtoneURI)
ringtone.play()
buttonClose.setOnClickListener {
if (ringtone.isPlaying) ringtone.stop()
}
}
}
- Now, a BrodcastReceiver class is required to monitor the created process in the background and perform the necessary actions. Since this process works even in sleep state, a Flag must be added to the defined intent.
override fun onReceive(context: Context, intent: Intent) {
val intent = Intent(context, AlarmActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
- Finally, the user has to choose a time. For this, action must be taken over TimePickerDialog. As you can see, when the button defined on XML is pressed, a time is selected with the TimePickerDialog. The selected time value is used in onTimeListener and AlarmManager operations.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val btnTime = findViewById<Button>(R.id.button)
btnTime.setOnClickListener {
val calendar = Calendar.getInstance()
val timePickerDialog = TimePickerDialog(this,
onTimeListener,
calendar[Calendar.HOUR_OF_DAY],
calendar[Calendar.MINUTE],
true)
timePickerDialog.show()
}
}
var onTimeListener = TimePickerDialog.OnTimeSetListener { view, hour, minute ->
val selectedTime = Calendar.getInstance()
selectedTime[Calendar.HOUR_OF_DAY] = hour
selectedTime[Calendar.MINUTE] = minute
val intent = Intent(this, MyReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(this, 1, intent, 0)
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
alarmManager[AlarmManager.RTC_WAKEUP, selectedTime.timeInMillis] = pendingIntent
}
}
This is how advanced topics in the Android field are. You can use the official documents for more detailed information.
Thank You
IBRAHIM CAN ERDOGAN