Monkey patching is a technique used in some programming languages that allows developers to modify or extend the behavior of existing classes at runtime, without the need to modify the original source code. This technique can be useful for adding new functionality to existing code, or for patching bugs or other issues in third-party libraries or frameworks.
In Kotlin, monkey patching refers to the ability to add, modify, or replace methods or properties of an existing class at runtime, without modifying the original class source code. However, unlike in some other languages, such as Python, monkey patching is not a common practice in Kotlin, and it is generally discouraged due to the potential for introducing unexpected behavior and making the code more difficult to maintain.
In Kotlin, monkey patching can be achieved in several ways, including:
Extension functions
Kotlin supports extension functions, which allow you to add new methods to an existing class without modifying the class itself. To define an extension function, you simply declare a function outside of the original class and prefix its name with the class name. For example, to add a new method called “greet” to the String class, you could define the following extension function:
Kotlin
funString.greet() {println("Hello, $this!")}
This function can then be called on any String instance as if it were a method of the original class:
Kotlin
val name = "softAai"name.greet() // prints "Hello, softAai!"
Reflection
Kotlin also supports reflection, which allows you to inspect and modify the properties and methods of a class at runtime. This can be used to monkey patch a class by dynamically adding or modifying its properties or methods. For example, to add a new method called “scream” to the String class using reflection, you could define the following code:
This code would add a new method to the String class called “scream”, which replaces the value of the “value” field with a new character array containing the string “AHHHHH”. However, it’s worth noting that this approach can be complex and error-prone, and should be used with caution.
Proxy classes
Another way to achieve monkey patching in Kotlin is to use proxy classes, which are classes that intercept method calls and modify their behavior at runtime. This can be useful for adding new functionality to existing classes or for patching bugs or other issues in third-party libraries or frameworks. To create a proxy class, you would typically define a new class that implements the same interface or extends the same base class as the original class, and then override the desired methods to add or modify their behavior.
Real-world examples of monkey patching
Here are some real-world examples of monkey patching in Kotlin:
Adding a new method to an existing class using extension functions
Let’s say you’re working on a project that uses a third-party library that provides a “Person” class with some basic functionality, but you need to add a new method to the class that isn’t provided by the library. You could use an extension function to monkey patch the “Person” class and add the new method:
Now, you can call the “greet” method on any instance of the “Person” class, even though it’s not part of the original class definition:
Kotlin
val person = Person("amol", 25)person.greet() // prints "Hello, amol!"
Modifying the behavior of an existing class using reflection
Let’s say you’re working on a project that uses a third-party library that provides a “Math” class with some basic math functions, but you need to modify the behavior of the “sqrt” function to always return a specific value(here 3.0). You could use reflection to monkey patch the “Math” class and modify the “sqrt” method:
Now, whenever the “sqrt” method is called on the “Math” class, it will always return 3.0, regardless of the input value.
Adding new functionality to an existing class using proxy classes:
Let’s say you’re working on a project that uses a third-party library that provides a “Database” class with some basic database functionality, but you need to add a new method to the class that isn’t provided by the library. You could use a proxy class to monkey patch the “Database” class and add the new method:
Now, instead of using the original “Database” class provided by the library, you can use the “DatabaseProxy” class, which extends the original class and adds the new “backup” method:
Kotlin
val database = DatabaseProxy(Database())database.query("SELECT * FROM users")database.backup()
Note that while these examples demonstrate how monkey patching can be achieved in Kotlin, it’s generally recommended to avoid this technique as much as possible, as it can make the code more difficult to understand and maintain. Instead, it’s often better to work with the original source code or use Kotlin’s built-in features, such as extension functions, to add new functionality to existing classes.
One potential issue with monkey patching that should be noted is that it can lead to naming collisions if multiple patches are applied to the same class or library. This can make it difficult to keep track of which patches are being used and can lead to unpredictable behavior. To avoid this, it’s important to use clear and consistent naming conventions for monkey patches and to document them clearly in the codebase.
Another consideration with monkey patching is that it can potentially introduce security vulnerabilities if patches are used to modify sensitive or critical parts of the codebase. It’s important to carefully review and test any monkey patches before applying them in production, and to consider alternative approaches if there are security concerns.
Pros:
Flexibility: Monkey patching allows developers to modify or add functionality to existing classes or libraries without modifying their original source code, which can be especially useful when working with third-party libraries or legacy code.
Rapid prototyping: Monkey patching can also be useful for quickly prototyping or testing new features or functionality without modifying the original source code, allowing developers to experiment and iterate more quickly.
Code reusability: Monkey patching can help reduce code duplication by allowing developers to extend the functionality of existing classes or libraries, rather than writing new code from scratch.
Cons:
Readability and maintainability: Monkey patching can make code more difficult to read and understand, especially for other developers who are not familiar with the codebase. Additionally, since monkey patching modifies existing code at runtime, it can make debugging and maintaining the code more difficult.
Unpredictable behavior: Since monkey patching modifies existing code at runtime, it can lead to unpredictable behavior and unintended consequences. This is especially true when patching code from third-party libraries, as it can be difficult to know how the patch will interact with other parts of the codebase.
Dependency on implementation details: Monkey patching often relies on implementation details of the existing code, such as private methods or fields, which can change between different versions or implementations of the code. This can lead to code that is fragile and difficult to maintain over time.
Conclusion
In general, monkey patching should be used sparingly and only when necessary, as it can have unintended consequences and make code more difficult to maintain. If possible, it’s often better to work with the original source code or use Kotlin’s built-in features, such as extension functions, to add new functionality to existing classes.
Overall, while monkey patching can be a useful technique in some cases, it should be used with caution and with a thorough understanding of its benefits and drawbacks. Developers should carefully consider whether monkey patching is the best approach for their particular use case and should be prepared to document and maintain any patches they create over time.
In Kotlin, an inner class is a class that is nested inside another class, and it has access to the outer class’s properties and methods. Inner classes are useful when you need to group related classes together or when you need to access the outer class’s properties and methods from within the inner class. In this article, we’ll cover all aspects of inner classes in Kotlin.
Declaring an Inner Class
To declare an inner class in Kotlin, you simply use the keyword inner before the class declaration. Here’s an example:
Kotlin
classOuterClass {innerclassInnerClass {// inner class properties and methods }}
In the example above, we have an OuterClass with an inner class called InnerClass.
Relationship between outer and inner class
The relationship between an outer class and an inner class is not an “is-a” relationship, but a “has-a” relationship (composition or aggregation). That means Inner classes can be used when one type of object cannot exist without another type of object. For example, if a university has several departments, the department class can be declared inside the university class since departments cannot exist without the university.
Accessing Outer Class Members
Since an inner class has access to the outer class’s properties and methods, you can access them using this keyword with the name of the outer class. Here’s an example:
Here, we have an OuterClass with a private property called outerProperty. We also have an inner class called InnerClass with a method called printOuterProperty that prints the outerProperty using the this@OuterClass syntax.
Creating an Inner Class Instance
To create an instance of an inner class, you first need to create an instance of the outer class. Here’s an example:
Kotlin
classOuterClass {innerclassInnerClass {// inner class properties and methods }}funmain() {val outer = OuterClass()valinner = outer.InnerClass()}
In the example above, we have an OuterClass with an inner class called InnerClass. We create an instance of the OuterClass called outer and then create an instance of the InnerClass called inner using the outer.InnerClass() syntax.
Types of Inner Classes
1. Nested classes:
Nested classes are declared using the class keyword and are by default static. They can access only the members of the outer class that are static. Here’s an example:
Kotlin
classOuter {privateval outerMember: Int = 1companionobject {constval companionMember = "This is a companion object member." }classNested {funprint() {println("This is a nested class.") } }}
In this example, the Nested class is nested within the Outer class, and the companion object can be accessed without an instance of the Outer class. The companion object can also access the private members of the Outer class. the Nested class is a static nested class that can be accessed without an instance of the outer class. You can create an instance of the Nested class and access the print method like this:
Kotlin
val nested = Outer.Nested()nested.print() // This is a nested class.
And you can access the companion object member like this:
Kotlin
println(Outer.companionMember) // This is a companion object member.
It can only access the outerMember if it is also declared as static.
2. Inner classes:
Inner classes are declared using the inner keyword and are by default non-static. They can access both instance and static members of the outer class. Here’s an example:
Kotlin
classOuter {privateval outerMember: Int = 1innerclassInner {funprint() {println("This is an inner class with access to outerMember: $outerMember.") } }}
In this example, the Inner class is an inner class that can access both instance and static members of the Outer class. You need to create an instance of the Outer class first before you can create an instance of the Inner class:
Kotlin
val outer = Outer()valinner = outer.Inner()inner.print() // This is an inner class with access to outerMember: 1.
Anonymous inner classes:
Anonymous inner classes are unnamed inner classes that are declared and instantiated in a single expression. They are often used for implementing interfaces or extending classes in a concise way. Here’s an example:
In this example, the OnClickListener interface is implemented as an anonymous inner class and passed to the setOnClickListener method of the Button class. This allows us to implement the interface inline without having to define a separate class.
Local inner classes:
Local inner classes are declared inside a block of code, such as a function or a method, and can access both local variables and members of the enclosing class. Here’s an example:
Kotlin
funouterFunction() {val outerMember: Int = 1classInner {funprint() {println("This is a local inner class with access to outerMember: $outerMember.") } }valinner = Inner()inner.print() // This is a local inner class with access to outerMember: 1.}
In this example, the Inner class is a local inner class declared inside the outerFunction function. It can access the outerMember variable of the function, as well as any other members of the outerFunction class.
Use cases of inner classes
Let’s discuss nested and anonymous inner classes in different scenarios to understand their use cases better.
1. Various combinations of nested classes and interfaces
In Kotlin, you can declare nested classes and interfaces inside other classes, and you can use them in various combinations. Here are some examples of different combinations of nested classes and interfaces:
In this example, we declare an interface MyInterface inside the class MyClass. This interface can be implemented by any class, but it is only accessible through an instance of MyClass.
1.b) Nested class inside an interface:
Kotlin
interfaceMyInterface {classNestedClass {fundoSomething() {println("NestedClass is doing something") } }}
In this example, we declare a nested class NestedClass inside the MyInterface. This nested class can be accessed without an instance of MyInterface. We can create an instance of this class and call its doSomething() method as follows:
Kotlin
val nestedClass = MyInterface.NestedClass()nestedClass.doSomething() // prints "NestedClass is doing something"
In this example, we declare an interface MyInterface inside the nested class MyNestedClass. This interface can be implemented by any class, but it is only accessible through an instance of MyClass.MyNestedClass.
In this example, we declare a nested class MyOuterNestedClass inside the MyClass, and a nested class MyInnerNestedClass inside the MyOuterNestedClass. This nested class can be accessed without an instance of MyClass. We can create an instance of this class and call its doSomething() method as follows:
Kotlin
val nestedClass = MyClass.MyOuterNestedClass.MyInnerNestedClass()nestedClass.doSomething() // prints "MyInnerNestedClass is doing something"
2. Anonymous Inner class
2.a)Anonymous Inner class that extends a class:
Kotlin
openclassSuperClass {openfunsayHello() {println("Hello from SuperClass") }}funmain() {val obj = object : SuperClass() {overridefunsayHello() {println("Hello from anonymous inner class") } } obj.sayHello() // Output: Hello from anonymous inner class}
You can create an anonymous inner class that extends a class using the object keyword followed by the class name in parentheses and the body of the class in curly braces.
2.b) Anonymous Inner class that implements an interface:
You can create an anonymous inner class that implements an interface using the object keyword followed by the interface name and the body of the class in curly braces.
2.c) Anonymous Inner class that is defined inside arguments:
You can define an anonymous inner class inside the arguments of a function call or constructor call using the object keyword followed by the class name, interface name, or a generic type and the body of the class in curly braces.
3. Access inner classes from different areas of the outer class
3.a) Access from instance methods or properties of the outer class:
Use this@Outer followed by the dot operator and the name of the inner class.
In all these cases, you can use this keyword to refer to the instance of the current class (inner or outer) and super keyword to refer to the superclass of the current class.
4. Inner Classes with Inheritance
Inner classes can also inherit from other classes. When you inherit from a class in an inner class, you can access the outer class’s properties and methods using thesuperkeyword. Here’s an example:
In this example, the Person class has an open method called greet() which is overridden in the Student class using the override keyword. Within the Student class, an Internship inner class is defined that extends the Job inner class of the Person class.
In the Internship class, the super keyword is used to refer to the printDetails() method of the Job class in the Person class. This allows the Internship class to inherit and extend the behavior of the Job class while also adding its own functionality.
5. Inner classes to improve encapsulation
5.a) Access to Outer Class Properties and Methods
Kotlin
classOuterClass(privateval name: String) {privateval id: Int = 123innerclassInnerClass {funprintOuterName() {println(name) // can access name property of outer class }funprintOuterId() {println(id) // can access id property of outer class } }}
Here InnerClass can access the private name and id properties of the OuterClass. This allows you to keep these properties hidden from the rest of the codebase, while still allowing the InnerClass to use them as needed.
In this example, the Message inner class represents a message to be sent over the network connection. By grouping this related functionality into its own class, you can make your code more organized and easier to understand.
In above example, the ItemSelectionHandler inner class handles item selection events in the ItemListView. By separating this functionality into its own class, you can make your code more modular and easier to test.
5.d) Access Control
Kotlin
classOuterClass {privateval name: String = "John"innerclassInnerClass {privateval age: Int = 30// private to InnerClassfunprintName() {println(name) // can access name property of outer class }funprintAge() {println(age) // can access age property of inner class } }}
In this example, the age property of the InnerClass is private to that class, and cannot be accessed from outside. This helps to prevent unwanted access to certain parts of your codebase and improve the security of your application.
Advantages of Inner Classes in Kotlin
Encapsulation: Inner classes can access private members of the enclosing class, which helps to encapsulate the code and restrict access to certain parts of the code.
Code organization: Inner classes help to organize the code and keep related code together. This makes the code easier to read and understand.
Improved code reuse: Inner classes can be reused in multiple places within the enclosing class, which reduces code duplication and improves code maintainability.
Improved readability: Inner classes can be used to define small, self-contained units of code that are easier to read and understand.
Access to outer class: Inner classes have access to the methods and variables of the outer class, which can be useful in certain situations.
Disadvantages of Inner Classes in Kotlin
Increased complexity: Inner classes can make the code more complex, especially when multiple layers of inner classes are used.
Performance overhead: Inner classes can result in additional memory usage and performance overhead, especially if the inner class is not static.
Tight coupling: Inner classes can create tight coupling between the inner class and the outer class, which can make it difficult to reuse the code in other contexts.
Potential for memory leaks: Inner classes can create memory leaks if they hold references to the outer class, as this can prevent the outer class from being garbage collected.
Name conflicts: Inner classes can have the same name as classes in the outer scope, which can lead to naming conflicts and make the code harder to read and understand.
Clean Architecture and MVVM Architecture are two popular architectural patterns for building robust, maintainable, and scalable Android applications. In this article, we will discuss how to implement Clean Architecture and MVVM Architecture in an Android application using Kotlin. We will cover all aspects of both architectures in-depth and explain how they work together to create a robust application.
Clean Architecture
Clean Architecture is a software design pattern that emphasizes separation of concerns and the use of dependency injection. It divides an application into layers, with each layer having a specific responsibility. The layers include:
Presentation Layer
Domain Layer
Data Layer
The Presentation Layer is responsible for the user interface and interacts with the user. The Domain Layer contains business logic and rules. The Data Layer interacts with external sources of data.
The Clean Architecture pattern is designed to promote testability, maintainability, and scalability. It reduces coupling between different parts of an application, making it easier to modify or update them without affecting other parts of the application.
MVVM Architecture
MVVM stands for Model-View-ViewModel. It is a software design pattern that separates an application into three layers: Model, View, and ViewModel. The Model represents the data and business logic. The View represents the user interface. The ViewModel acts as a mediator between the Model and the View. It exposes data from the Model to the View and handles user input from the View.
MVVM Architecture promotes separation of concerns, testability, and maintainability. It is designed to work with data binding and makes it easy to update the user interface when data changes.
Combining Clean and MVVM Architecture
Clean Architecture and MVVM Architecture can be used together to create a robust, maintainable, and scalable Android application. The Presentation Layer in Clean Architecture corresponds to the View and ViewModel in MVVM Architecture. The Domain Layer in Clean Architecture corresponds to the Model in MVVM Architecture. The Data Layer in Clean Architecture corresponds to the Data Layer in MVVM Architecture.
Implement Clean and MVVM Architecture
Let’s build one demo app to implement Clean and MVVM Architecture. we will create a simple app that displays a list of movies and allows the user to view the details of each movie. We will use the Movie Database API as our data source.
Building this MVVM demo app using Clean Architecture, MVVM, Kotlin, Coroutines, Room, Hilt, Retrofit, Moshi, Flow, and Jetpack Compose.
Set up the project
Create a new project in Android Studio and add the necessary dependencies for MVVM Architecture, such as room, hilt, and ViewModel.
Create initial packages for each layer of Clean Architecture: Presentation, Domain, and Data. Inside each package, create sub-packages for specific functionalities of the layer.
├── data
│ ├── repository
│ └── source
│ ├── local
│ │ ├── datastore
│ │ └── roomdb
│ └── remote
├── di
│ ├── movies
│ └── moviedetails
├── domain
│ ├── model
│ ├── repository
│ └── usecase
└── presentation
├── ui
└── viewmodel
In this hierarchy, we have:
data package which contains the repository and source packages.
The repository package contains classes responsible for fetching data from source and returning it to domain.
The source package contains local and remote packages.
The local package contains classes responsible for accessing data from local data storage, such as datastore and roomdb.
The remote package contains classes responsible for accessing data from remote data storage, such as APIs.
di package which contains the movies and moviedetails packages.
These packages contain classes responsible for dependency injection related to movies and moviedetails modules.
domain package which contains the model, repository, and usecase packages.
The model package contains classes representing the data model of the application.
The repository package contains interfaces defining the methods that the repository classes in data package must implement.
The usecase package contains classes responsible for defining the use cases of the application, by using repository interfaces and returning the result to the presentation layer.
presentation package which contains the ui and viewmodel packages.
The ui package contains classes responsible for the user interface of the application, such as activities, fragments, and views.
The viewmodel package contains classes responsible for implementing the ViewModel layer of the application, which holds data related to the UI and communicates with the usecase layer.
Identify JSON Response
Identify the JSON response from the URL, Before making a network request to the URL, please use your own API Key as mine is an invalid key. Then examine the JSON response using an Online JSON Viewer to identify its structure. Once you have identified the structure of the response, create Kotlin DTOs for the response and place them in the remote package.
DTOs, Entities, and Domain Models:
In our Android application, we will have different types of data models. These models include DTOs, Entities, and Domain Models.
DTOs (Data Transfer Objects) are used to transfer data between different parts of the application. They are typically used to communicate with a remote server or API.
Entities represent the data models in our local database. They are used to persist data in our application.
Domain Models represent the business logic in our application. They contain the logic and rules that govern how data is processed in the application.
By using these different types of models, we can separate our concerns and ensure that each model is responsible for its own functionality. This makes our code more modular and easier to maintain.
Mapper Functions:
In our Android application, we will often need to convert between different types of models. For example, we might need to convert a DTO to an Entity or an Entity to a Domain Model. To do this, we can use Mapper Functions.
Mapper Functions are used to convert data between different models. They take an input model and convert it to an output model. By using Mapper Functions, we can ensure that our code is organized and maintainable, and we can easily convert between different models as needed.
Define DTOs
We can create Kotlin data transfer objects (DTOs) to represent the data and place them into the remote package, as it represents data fetched from a remote data source
To fetch movies from the Movie Database API, we will use Retrofit to define an interface that defines the API endpoints. We will also use Moshi to deserialize the JSON responses into our Movie data class. Here\’s an example of how to define the API interface:
Kotlin
package com.softaai.mvvmdemo.data.source.remoteimport com.softaai.mvvmdemo.data.source.remote.dto.PopularMoviesDtoimport retrofit2.http.GET/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */interfaceMovieApiService {@GET("movie/popular")suspendfungetPopularMovies(): PopularMoviesDtocompanionobject {constval BASE_URL: String = "https://api.themoviedb.org/3/" }}
Here, we are using the @GET annotation to define the API endpoint, and the suspend keyword to indicate that this function should be called from a coroutine. We are also using the deserialized PopularMovieDto data class.
Note → We used PopularMoviesDto data class directly instead of wrapping it in a Response or Resource class. This is because it is assumed that the API response will always contain the expected data structure and any errors in the API call will be handled by catching exceptions, another reason is we are not tightly coupling our app to the API response structure and can modify the response format without affecting the rest of the app.
Define Resource Sealed Class
Resource Sealed Classes are used to represent the state of a request or operation that can either succeed or fail. They allow us to handle different states of an operation, such as loading, success, or error, in a more organized way. Typically, a Resource Sealed Class contains three states:
Loading: When the operation is in progress.
Success: When the operation is successful and data is available.
Error: When the operation fails.
Kotlin
package com.softaai.mvvmdemo.data.source.remote/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */sealedclassResource<T>(valdata: T? = null, val message: String? = null) {classLoading<T>(data: T? = null) : Resource<T>(data)classSuccess<T>(data: T?) : Resource<T>(data)classError<T>(message: String, data: T? = null) : Resource<T>(data, message)}
By using Resource Sealed Classes, we can easily handle different states of an operation in our ViewModel without writing lots of boilerplate code.
Implement Interceptor for network requests
The purpose of the interceptor is to add an API key query parameter to every outgoing network request.
Kotlin
package com.softaai.mvvmdemo.data.source.remoteimport okhttp3.Interceptorimport okhttp3.Response/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */classRequestInterceptor : Interceptor {overridefunintercept(chain: Interceptor.Chain): Response {val originalRequest = chain.request()val newUrl = originalRequest.url .newBuilder() .addQueryParameter("api_key","04a03ff73803441c785b1ae76dbdab9c"//TODO Use your api key this one invalid ) .build()val request = originalRequest.newBuilder() .url(newUrl) .build()return chain.proceed(request) }}
The RequestInterceptor class implements the Interceptor interface provided by the OkHttp library, which allows it to intercept and modify HTTP requests and responses.
In the intercept method, the incoming chain parameter represents the chain of interceptors and the final network call to be executed. The method first retrieves the original request from the chain using chain.request(). It then creates a new URL builder from the original request URL and adds a query parameter with the key \”api_key\” and a specific value to it. Here use your own api_key as existing key is invalid
Next, it creates a new request by calling originalRequest.newBuilder() and setting the new URL with the added query parameter using .url(newUrl). Finally, it calls chain.proceed(request) to execute the modified request and return the response.
Overall, this interceptor helps to ensure that every network request made by the app includes a valid API key, which is required for authentication and authorization purposes.
Implementation of Room
Room is a powerful ORM (Object-Relational Mapping) library that makes it easy to work with a SQLite database in Android. It provides a high-level API for working with database tables, queries, and transactions, as well as support for Flow, LiveData, and RxJava for reactive programming.
Define the Entity
First, we’ll define the MovieEntity class, which represents the movies table in our local database. We annotate the class with @Entity and specify the table name and primary key. We also define the columns using public properties.
Kotlin
package com.softaai.mvvmdemo.data.source.local.roomdb.entityimport androidx.room.ColumnInfoimport androidx.room.Entityimport androidx.room.PrimaryKeyimport com.softaai.mvvmdemo.domain.model.Movie/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */@Entity(tableName = MovieEntity.TABLE_NAME)dataclassMovieEntity(@PrimaryKeyval id: Int,val title: String,val overview: String,@ColumnInfo(name = "poster_url") val posterUrl: String,@ColumnInfo(name = "release_date") val releaseDate: String) {funtoMovie(): Movie {returnMovie( title = title, overview = overview, posterUrl = posterUrl, releaseDate = releaseDate ) }companionobject {constval TABLE_NAME = "movie" }}
We have another entity for API response, which contains a movie entity list
Next, we’ll define the MovieDao interface, which provides the methods to interact with the movies table. We annotate the interface with @Dao and define the query methods using annotations such as @Query, @Insert, and @Delete.
Kotlin
package com.softaai.mvvmdemo.data.source.local.roomdb.daoimport androidx.room.Daoimport androidx.room.Insertimport androidx.room.OnConflictStrategyimport androidx.room.Queryimport com.softaai.mvvmdemo.data.source.local.roomdb.entity.MovieEntity/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */@DaointerfaceMovieDao {@Insert(onConflict = OnConflictStrategy.REPLACE)suspendfuninsertMovieList(movies: List<MovieEntity>)@Query("SELECT * FROM ${MovieEntity.TABLE_NAME}")suspendfungetMovieList(): List<MovieEntity>@Query("DELETE FROM ${MovieEntity.TABLE_NAME}")suspendfundeleteAll()}
Note ->Here also, we are not using any wrappers like Flow, Response, or Resource. The reason behind this is that we are keeping the repository layer decoupled from the data sources (local or remote) and allowing for easier testing and evolution. In this specific case, it is a simple synchronous database operation, as the data is being retrieved from the local database using Room. Room already provides the functionality to perform asynchronous database operations in the background, so we do not need to use any additional wrappers like Flow or Resource. We can simply call the getMovieList() method from a coroutine and retrieve the list of MovieEntity objects.
Define Type Converter
In Room, a type converter is a way to convert non-primitive types (such as Date or custom objects in our case List<MovieEntity>) to primitive types that can be stored in the SQLite database.
To use a type converter in Room, you need to create a class that implements the TypeConverter interface, which has two methods: toType() and fromType(). The toType() method converts a non-primitive type to a primitive type, while the fromType() method converts the primitive type back to the non-primitive type.
To use this TypeConverter, you need to annotate the field or property that needs to be converted with the @TypeConverters annotation, specifying the converter class.
Define the Database
Finally, we’ll define the MovieDatabase class, which represents the entire local database. We annotate the class with @Database and specify the list of entities and version number. We also define a singleton instance of the database using the Room.databaseBuilder method.
Usually In the Domain Layer, we define the interfaces for the Repository and Use Case (In our case, skipped for the Use Case). These interfaces define the methods that will be used to interact with the data layer. The Repository interface defines the methods that will be used to retrieve and save data, while the Use Case interface defines the business logic that will be performed on the data.
We will create a MovieRepository interface that defines the methods for fetching movies:
Kotlin
package com.softaai.mvvmdemo.domain.repositoryimport com.softaai.mvvmdemo.data.source.remote.Resourceimport com.softaai.mvvmdemo.domain.model.Movieimport kotlinx.coroutines.flow.Flow/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */interfaceMovieRepository {fungetPopularMovies(): Flow<Resource<List<Movie>>>}
We are returning a Flow<Resource<List<Movie>>> from the getPopularMovies() function. The Flow will emit the result of the API call asynchronously, and the Resource class will hold either the list of movies or an error.
Implement a Repository interface in the Data Layer
We define interfaces for the Repository and Use Case in the Domain Layer and these interfaces will be implemented in the Data Layer. By separating the interfaces from their implementations, we can easily swap out the data layer implementation if needed. This allows us to easily switch between different data sources, such as a local database or a remote API, without having to modify the business logic layer.
In our example, we will create an implementation of the MovieRepository interface that uses Retrofit and Moshi to fetch the popular movies:
Kotlin
package com.softaai.mvvmdemo.data.repositoryimport com.softaai.mvvmdemo.data.source.local.roomdb.dao.MovieDaoimport com.softaai.mvvmdemo.data.source.local.roomdb.dao.PopularMoviesDaoimport com.softaai.mvvmdemo.data.source.remote.MovieApiServiceimport com.softaai.mvvmdemo.data.source.remote.Resourceimport com.softaai.mvvmdemo.domain.model.Movieimport com.softaai.mvvmdemo.domain.repository.MovieRepositoryimport kotlinx.coroutines.flow.Flowimport kotlinx.coroutines.flow.flowimport retrofit2.HttpExceptionimport java.io.IOException/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */classMovieRepositoryImplconstructor(privateval movieApiService: MovieApiService,privateval popularMoviesDao: PopularMoviesDao,privateval movieDao: MovieDao) : MovieRepository {overridefungetPopularMovies(): Flow<Resource<List<Movie>>> = flow {emit(Resource.Loading())try {fetchAndInsertPopularMovies(movieApiService, popularMoviesDao, movieDao) } catch (e: HttpException) {emit( Resource.Error( message = "Oops, something went wrong!" ) ) } catch (e: IOException) {emit( Resource.Error( message = "Couldn't reach server, check your internet connection." ) ) }// single source of truth we will emit data from db only and not directly from remoteemit(Resource.Success(getPopularMoviesFromDb(movieDao))) }privatesuspendfunfetchAndInsertPopularMovies( movieApiService: MovieApiService, popularMoviesDao: PopularMoviesDao, movieDao: MovieDao ) {val remotePopularMovies = movieApiService.getPopularMovies() popularMoviesDao.insertPopularMovies(remotePopularMovies.toPopularMoviesEntity()) movieDao.insertMovieList(remotePopularMovies.results.map { it.toMovieEntity() }) //now insert newly fetched data to db }privatesuspendfungetPopularMoviesFromDb(movieDao: MovieDao): List<Movie> {val newPopularMovies = movieDao.getMovieList().map { it.toMovie() }return newPopularMovies }}
Here, we are using the flow builder from the Kotlin coroutines library to emit the result of the API call asynchronously. We are also using the catch operator to catch any exceptions that might occur during the API call. If there is an error, we emit the error wrapped in the Resource.Error class.
Implement Use Case
I skipped implementing the Use Case in the Data Layer, such as the Repository, and instead implemented it directly in the Domain Layer for this small assignment. However, in a bigger project, it is important to implement it properly in the Data Layer.
Kotlin
package com.softaai.mvvmdemo.domain.usecaseimport com.softaai.mvvmdemo.data.source.remote.Resourceimport com.softaai.mvvmdemo.domain.model.Movieimport com.softaai.mvvmdemo.domain.repository.MovieRepositoryimport kotlinx.coroutines.flow.Flow/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */classGetPopularMovies(privateval movieRepository: MovieRepository) {operatorfuninvoke(): Flow<Resource<List<Movie>>> {return movieRepository.getPopularMovies() }}
The GetPopularMovies class is a use case class in the domain layer that provides a way to retrieve a list of popular movies from the MovieRepository. By using this class, we can easily retrieve the list of popular movies by calling its invoke() method an operator function, which returns a Flow. We can then collect the items emitted by the Flow and handle the different states of the data using the Resource class.
Add Hilt Modules for Dependency Injection
Hilt is a dependency injection framework that makes it easy to manage dependencies in Android apps. It is built on top of Dagger, a popular dependency injection library, and provides a simpler, more streamlined API for configuring and injecting dependencies.
To inject dependencies into our ViewModel and Repository, we’ll use Hilt for Dependency Injection. Since we have already added the Hilt dependency in the gradle file, we can now directly annotate our Application class with @HiltAndroidApp:
Kotlin
package com.softaai.mvvmdemoimport android.app.Applicationimport dagger.hilt.android.HiltAndroidApp/** * Created by amoljp19 on 4/19/2023. * softAai Apps. */@HiltAndroidAppclassMvvmDemoApp : Application() {}
Define Hilt modules
Create a Kotlin object for each module and annotate it with @Module. In each module, define one or more provider methods that create instances of your dependencies and annotate them with @Provides.
This is a Hilt module called MoviesNetworkModule, which is used for providing dependencies related to network communication with the MovieApiService. The module is annotated with @Module and @InstallIn(SingletonComponent::class), which means that it will be installed in the SingletonComponent and has the scope of the entire application.
The module provides the following dependencies:
OkHttpClient: This dependency is provided by a method called provideOkHttpClient, which returns an instance of OkHttpClient that is built with RequestInterceptor and HttpLoggingInterceptor.
MovieApiService: This dependency is provided by a method called provideRetrofitService, which takes an instance of OkHttpClient as a parameter and returns an instance of MovieApiService. This method builds a Retrofit instance using MoshiConverterFactory for JSON parsing and the provided OkHttpClient, and creates a MovieApiService instance using the Retrofit.create method.
The @Singleton annotation is used on both provideOkHttpClient and provideRetrofitService methods, which means that Hilt will only create one instance of each dependency and provide it whenever it is needed.
By using these @Provides methods, we can provide these dependencies to any component in our app by simply annotating the constructor of that component with @Inject.
MoviesDatabaseModule
Kotlin
package com.softaai.mvvmdemo.di.moviesmoduleimport android.app.Applicationimport com.softaai.mvvmdemo.data.source.local.roomdb.MovieDatabaseimport dagger.Moduleimport dagger.Providesimport dagger.hilt.InstallInimport dagger.hilt.components.SingletonComponentimport javax.inject.Singleton/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */@Module@InstallIn(SingletonComponent::class)classMoviesDatabaseModule {@Singleton@ProvidesfunprovideDatabase(application: Application) = MovieDatabase.getDatabase(application)@Singleton@ProvidesfunprovidePopularMoviesDao(database: MovieDatabase) = database.getPopularMoviesDao()@Singleton@ProvidesfunprovideMovieDao(database: MovieDatabase) = database.getMovieDao()}
Here we have defined a Hilt module called MoviesDatabaseModule which is annotated with @Module and @InstallIn(SingletonComponent::class). This means that this module will be installed in the SingletonComponent which has the scope of the entire application. By using these @Provides methods, we can provide these dependencies to any component in our app by simply annotating the constructor of that component with @Inject.
For example, if we want to use PopularMoviesDao in our MovieRepository, we can simply annotate the constructor of MovieRepository with @Inject and pass PopularMoviesDao as a parameter:
By doing this, Hilt will automatically provide the PopularMoviesDao, MovieDao, and MovieApiService objects to our MovieRepository whenever it is needed.
This is a Dagger Hilt module for providing the MovieRepository implementation to the app. The module is annotated with @Module and @InstallIn(SingletonComponent::class) which means that the MovieRepository will have a singleton scope throughout the app.
The @Provides method is defined to provide the MovieRepositoryImpl instance. This method takes three parameters: movieApiService of type MovieApiService, popularMoviesDao of type PopularMoviesDao, and movieDao of type MovieDao. These dependencies are injected into the constructor of MovieRepositoryImpl to create its instance.
MoviesUseCaseModule
Kotlin
package com.softaai.mvvmdemo.di.moviesmoduleimport com.softaai.mvvmdemo.domain.repository.MovieRepositoryimport com.softaai.mvvmdemo.domain.usecase.GetPopularMoviesimport dagger.Moduleimport dagger.Providesimport dagger.hilt.InstallInimport dagger.hilt.components.SingletonComponentimport javax.inject.Singleton/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */@Module@InstallIn(SingletonComponent::class)classMoviesUsecaseModule {@Provides@SingletonfunprovideGetPopularMoviesUseCase(repository: MovieRepository): GetPopularMovies =GetPopularMovies(repository)}
The GetPopularMovies use case by injecting the MovieRepository. The module is annotated with @InstallIn(SingletonComponent::class) which means it will be installed in the SingletonComponent of the application.
The provideGetPopularMoviesUseCase method is annotated with @Provides and @Singleton, indicating that it provides a singleton instance of the GetPopularMovies use case.
The repository parameter of the method is injected via constructor injection, as it is declared as a dependency of the GetPopularMovies constructor. The MovieRepository is provided by the MoviesRepositoryModule which is also installed in the SingletonComponent.
Define the ViewModel
Now, we can define the ViewModel that will be used to expose the movie data to the UI. We will create a MoviesViewModel class that extends the ViewModel class from the Android Architecture Components library:
This is the implementation of the MoviesViewModel, which is responsible for fetching and providing the list of popular movies to the UI layer. It uses the GetPopularMovies use case to fetch the data from the repository and updates the UI state based on the result of the operation.
Kotlin
package com.softaai.mvvmdemo.presentation.viewmodelimport com.softaai.mvvmdemo.domain.model.Movie/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */dataclassMovieUiState(val moviesList: List<Movie> = emptyList(),val isLoading: Boolean = false)
The @HiltViewModel annotation is used to inject dependencies into a ViewModel using Hilt. When a ViewModel is annotated with @HiltViewModel, Hilt generates a factory for the ViewModel and provides dependencies to the ViewModel via this factory. This way, the ViewModel can easily access dependencies, such as use cases or repositories, without the need to manually create and inject them.
The ViewModel uses a mutableStateOf() function to create a state object that can be updated from anywhere in the ViewModel. The state object is exposed as an immutable State object to the UI layer, which can observe it and update the UI accordingly.
The ViewModel also uses the viewModelScope to launch a coroutine that executes the use case, and observes the result of the operation using the onEach operator. Based on the result, the ViewModel updates the UI state accordingly, indicating whether the data is being loaded, whether it has been loaded successfully, or whether an error has occurred.
Define the Compose UI
First we define a MovieItem composable that displays a single movie item in a row. We are using CoilImage from the Coil library to display the movie poster image, and Row and Column composable functions to create the layout.
A composable function called CoilImage, displays an image using Coil library in Jetpack Compose. The function takes a String parameter called imageUrl which is the URL of the image to be displayed.
Finally, we will create a MoviesListScreen composable function that displays a list of popular movies using a LazyColumn.
Kotlin
package com.softaai.mvvmdemo.presentation.ui.composeimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.hilt.navigation.compose.hiltViewModelimport com.softaai.mvvmdemo.presentation.viewmodel.MoviesViewModel/** * Created by amoljp19 on 4/18/2023. * softAai Apps. */@ComposablefunMovieListScreen(moviesViewModel: MoviesViewModel = hiltViewModel()) {val state = moviesViewModel.state.valueLazyColumn( Modifier.fillMaxSize(), contentPadding = PaddingValues(bottom = 16.dp) ) {items(state.moviesList.size) { i ->MovieItem(movie = state.moviesList[i], onItemClick = {}) } }}
The Composable function MovieListScreen which takes a MoviesViewModel as a parameter and sets its default value using the hiltViewModel() function provided by the Hilt library that allows you to retrieve a ViewModel instance that is scoped to the current Compose component. This is useful because it allows you to inject dependencies directly into your ViewModel using the Hilt dependency injection system.
By using hiltViewModel() instead of creating a new instance of the MoviesViewModel class manually, you ensure that the instance of the MoviesViewModel used in the MovieListScreen composable is the same instance that is injected by Hilt into the ViewModel.
Inside the function, it gets the current state of the ViewModel using moviesViewModel.state.value and stores it in a variable called state.
It then creates a LazyColumn with Modifier.fillMaxSize() and a content padding of PaddingValues(bottom = 16.dp). Inside the LazyColumn, it creates a list of items using the items function, which iterates over the state.moviesList and creates a MovieItem for each movie.
The MovieItem composable is passed the movie object from the current iteration, and an empty lambda function onItemClick (which could be used to handle clicks on the item).
Putting it all together
Now, we can put all the pieces together in our MainActivity, which is annotated with the @AndroidEntryPoint annotation. This annotation is part of the Hilt library, and it allows Hilt to generate a component for the activity and provide dependencies to its fields and methods.
Kotlin
package com.softaai.mvvmdemoimport android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.material.MaterialThemeimport androidx.compose.material.Surfaceimport androidx.compose.material.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.tooling.preview.Previewimport com.softaai.mvvmdemo.presentation.ui.compose.MovieListScreenimport com.softaai.mvvmdemo.presentation.ui.theme.MVVMDemoThemeimport dagger.hilt.android.AndroidEntryPoint@AndroidEntryPointclassMainActivity : ComponentActivity() {overridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {MVVMDemoTheme {// A surface container using the 'background' color from the themeSurface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background ) {MovieListScreen() } } } }}
Inside the onCreate method, the setContent method is used to set the main content of the activity. In this case, the content is the MovieListScreen composable function, which displays a list of movies.
Note — I have provided proper guidance on how to display the list of movies. Now, you can continue building the movie details screen by following similar patterns as discussed earlier. If you face any issues or need any further assistance, feel free to ask me.
Conclusion
In this article, we have demonstrated how to build an Android app using Clean Architecture, MVVM, Kotlin, Room, Hilt, Retrofit, Moshi, Flow, and Jetpack Compose. We have covered all aspects of the app development process, including defining the data model, implementing the repository layer, defining the ViewModel, and defining the UI. By following these best practices, we can create robust and maintainable Android apps that are easy to test and evolve over time.
Note — I have provided proper guidance on how to display the list of movies. I now expect you to complete the remaining work by following the guidelines.
Jetpack Compose is a modern UI toolkit for building Android apps with Kotlin. One of the challenges of building UIs is loading and displaying images, which can be time-consuming and resource-intensive. Fortunately, the Coil library provides a simple and efficient way to load and display images in Jetpack Compose.
What is Coil?
The coil is a fast, lightweight, and modern image-loading library for Android. It was created by Chris Banes, a Google Developer Expert for Android. Some of the features of Coil include:
Loading images from URLs, files, and other sources.
Caching images to improve performance and reduce network usage.
Displaying fallback images if an image fails to load.
Supporting image transformations, such as resizing, cropping, and blurring.
Coil is designed to be easy to use and integrate into your app, with a small footprint and minimal dependencies.
Adding Coil to your Jetpack Compose Project
To use Coil in your Jetpack Compose project, you need to add the Coil dependency to your project’s build.gradle file:
After adding the dependency, you can use the rememberImagePainter function from Coil to load and display an image in your Jetpack Compose UI.
Loading and displaying images with Coil
To load and display an image with Coil, you need to call the rememberImagePainter function in your Jetpack Compose function. Here’s an example of how you can load and display an image from a URL:
In this example, we’re using the rememberImagePainter function from Coil to load and display an image from a URL. The data parameter specifies the URL of the image to load. The builder parameter is optional and allows you to customize the behavior of Coil, such as specifying a placeholder image or an error image.
Resizing images with Coil
Coil provides several functions for resizing images, including size, scale, and precision. Here’s an example of how you can resize an image using the size parameter:
In this example, we’re using the size parameter to resize the image to 100 x 100 dp.
Using request options with Coil
Coil provides a RequestOptions class that allows you to customize the behavior of the image loading process, such as setting a timeout, changing the cache strategy, or disabling crossfade animations. Here’s an example of how you can use request options with Coil:
In this example, we’re using the CircleCropTransformation() function from Coil to create a circular image, and then using the CircleShape modifier to clip the image to a circular shape.
Loading and displaying local images with Coil
Coil not only supports loading images from URLs but also from local files. Here’s an example of how you can load and display a local image with Coil:
In this example, we’re using the FileLocalSource class to specify the local image file. The builder parameter is optional and allows you to customize the behavior of Coil, such as enabling crossfade animations.
Conclusion
In this article, we’ve covered the basics of using Coil in Jetpack Compose to load and display images. We’ve seen how to add the Coil dependency to your project, how to load and display images with Coil, how to resize images, and how to use request options with Coil. We’ve also seen how to load and display local images with Coil.
Coil is a powerful and efficient library that can simplify the image loading and displaying process in your Jetpack Compose app. It’s lightweight, easy to use, and provides many customization options. I hope this article has been helpful in getting you started with using Coil in your Jetpack Compose app.
Access modifiers are an important part of object-oriented programming, as they allow you to control the visibility and accessibility of class members. In Kotlin, there are four access modifiers:
public
protected
private
internal
Each of these modifiers determines the level of visibility of a class member and how it can be accessed. In this article, we will cover each of these access modifiers in detail and discuss their interoperability with Java.
Public Visibility Modifier
In Kotlin, the public visibility modifier is used to specify that a class, method, property, or any other declaration is accessible from anywhere in your program. If you don’t specify any visibility modifier, your declaration is automatically considered public. For example:
Kotlin
classPerson {var name: String = ""funsayHello() {println("Hello, my name is $name") }}
The Person class and its properties and methods are public by default, meaning that they can be accessed from anywhere in your program or external modules.
Protected Visibility Modifier
In Kotlin, the protected modifier restricts the visibility of a member to its own class and its subclasses. This means that the member can only be accessed within the class where it is declared or in any subclasses of that class.
Here’s an example to illustrate how the protected modifier works:
Kotlin
openclassShape {protectedvar name: String = "Shape"protectedfungetName() {println(name) }}classRectangle : Shape() {funprintName() {getName() // Accessible because it is declared as protected in the Shape class }}funmain() {val shape = Shape() shape.getName() // Not accessible because getName() is declared as protected in the Shape classval rectangle = Rectangle() rectangle.printName() // Accessible because printName() calls getName() in the Rectangle class}
In this example, we have a Shape class with a protected property name and a protected function getName(). The Rectangle class extends the Shape class and has a function printName() that calls getName().
In the main() function, we create an instance of Shape and try to call getName(). This is not accessible because getName() is declared as protected in the Shape class. However, we can create an instance of Rectangle and call printName(), which in turn calls getName(). This is accessible because getName() is declared as protected in the Shape class and Rectangle is a subclass of Shape.
It’s important to note that the protected modifier only allows access to the member within its own class and its subclasses. It does not allow access from outside the class hierarchy, even if the class is in the same file or package.
Here’s an example to illustrate this:
Kotlin
package com.softaai.protectedopenclassShape {protectedvar name: String = "Shape"}classRectangle : Shape() {funprintName() {println(name) // Not accessible because name is declared as protected in the Shape class }}funmain() {val shape = Shape()println(shape.name) // Not accessible because name is declared as protected in the Shape class}
In this example, we have a Shape class with a protected property name and a Rectangle class that extends Shape. We also have a main() function in the same file that tries to access the name property of a Shape instance. However, this is not accessible because name is declared as protected in the Shape class, and the main() function is not part of the Shape class hierarchy.
Java interoperability of Protected Modifier
In Kotlin, a protected member is visible to its own class and its subclasses, just like in Java. When a Kotlin class is compiled to bytecode, its protected members are marked with the protected modifier in the bytecode, which allows Java classes to access them.
Here’s an example to illustrate this:
Kotlin
// Kotlin codeopenclassShape {protectedvar name: String = "Shape"}classRectangle : Shape() {funprintName() {println(name) // Accessible because name is declared as protected in the Shape class }}
Kotlin
// Java codepublicclassMain {public static void main(String[] args) { Rectangle rectangle = new Rectangle(); rectangle.printName(); // Accessible because it calls getName() which is declared as protected in the Shape class System.out.println(rectangle.name); // Not accessible because name is declared as protected in the Shape class }}
In this example, we have a Shape class with a protected property name and a Rectangle class that extends Shape. In the Kotlin code, the printName() function calls the name property, which is declared as protected in the Shape class. In the Java code, we create an instance of Rectangle and call printName(), which calls the name property. This is accessible because name is declared as protected in the Shape class, and Rectangle is a subclass of Shape.
However, we also try to access the name property directly from the Rectangle instance, which is not allowed because name is declared as protected and can only be accessed from within the class hierarchy.
Private Visibility Modifier
In Kotlin, you can use the private visibility modifier for classes, methods, properties, and any other declaration to restrict their visibility to the file where they are declared. This means that other files or classes in your program or external modules cannot access them. For example:
Kotlin
privateclassSecret {funtellSecret() {println("The secret is safe with me.") }}
In this example, the Secret class is private, and its tellSecret() method is accessible only within the file where it’s declared. Other files or external modules cannot access the Secret class or its methods.
Internal Visibility Modifier
The default visibility in Java, package-private, isn’t present in Kotlin. Kotlin uses packages only as a way of organizing code in namespaces; it doesn’t use them for visibility control. As an alternative, Kotlin offers a new visibility modifier, internal, which means “visible inside a module.” A module is a set of Kotlin files compiled together. It may be an IntelliJ IDEA module, an Android Studio or Eclipse project, a Maven or Gradle project, or a set of files compiled with an invocation of the Ant task. Internal visibility allows you to hide your implementation details and provide real encapsulation for your module. For example:
In this example, the MyClass class is marked as internal, and so are its myField field and myMethod method. This means that they can only be accessed within the same module.
Now, suppose you have another Kotlin module that wants to use MyClass, but can only access its public API:
Kotlin
package com.softaai.myothermoduleimport com.softaai.mymodule.MyClassclassMyOtherClass {privateval myClassObject = MyClass()fundoSomething() { myClassObject.myField = 100// compilation error: 'myField' is internal and cannot be accessed outside the module myClassObject.myMethod() // compilation error: 'myMethod' is internal and cannot be accessed outside the module }}
In this example, the MyOtherClass class is in a different module than MyClass, and can only access MyClass‘s public API. When MyOtherClass tries to access myField and myMethod, which are both marked as internal, it will result in compilation errors. This is because the internal modifier provides real encapsulation for the implementation details of the MyClass module, preventing external code from accessing and modifying its internal declarations.
Kotlin’s visibility modifiers and Java
Kotlin’s visibility modifiers and their default visibility rules are interoperable with Java, which means that Kotlin code can call Java code and vice versa.When Kotlin code is compiled to Java bytecode, the visibility modifiers are preserved and have the same meaning as in Java(one exception: a private class in Kotlin is compiled to a package-private declaration in Java). This means that you can use Kotlin declarations from Java code as if they were declared with the same visibility in Java. For example, a Kotlin class declared as public will be accessible from Java code as a public class.
However, there is one exception to this rule. In Kotlin, a private class is compiled to a package-private declaration in Java. This means that the class is not visible outside of the package, but it is visible to other classes within the same package.
The internal modifier in Kotlin is a bit different from the other modifiers, because there is no direct analogue in Java. In Java, package-private visibility is a different concept that does not correspond exactly to Kotlin’s internal visibility.
When Kotlin code with an internal modifier is compiled to Java bytecode, the internal modifier becomes public. This is because a module in Kotlin may contain declarations from multiple packages, whereas in Java, each package is self-contained.
This difference in visibility between Kotlin and Java can sometimes lead to unexpected behavior. For example, you may be able to access an internal class or a top-level declaration from Java code in another module, or a protected member from Java code in the same package. These scenarios are similar to how you would access these elements in Java.
However, it’s important to note that the names of internal members of a class are mangled. This means that they may look ugly in Java code, and are not meant to be used directly by Java developers. The purpose of this is to avoid unexpected clashes in overrides when you extend a class from another module, and to prevent accidental use of internal classes.
Let’s say you have a Kotlin class with an internal function:
Kotlin
package com.softaai.internalclassMyClass {internalfunmyFunction() {println("Hello from myFunction!") }}
When compiled to bytecode and viewed from Java, the myFunction method will have a mangled name that includes the package name and a special prefix to indicate its visibility. The mangled name might look something like this:
Kotlin
publicfinal void com.softaai.internal.MyClass$myFunction() {// implementation of myFunction}
As you can see, the mangled name includes the package name and the name of the class, with a $ separator followed by the original name of the method. This naming convention helps to prevent naming clashes and ensures that the method is only accessible within the module where it was defined.
Conclusion
Kotlin’s access modifiers provide a way to control the visibility of code within a module and ensure better encapsulation. They also map well to Java access modifiers, which makes Kotlin code fully interoperable with Java. When working with both Kotlin and Java code, it’s important to understand how access modifiers are represented in both languages to ensure that code is visible only where it’s intended to be visible.
In Effective Java by Joshua Bloch (Addison-Wesley, 2008), one of the best-known books on good Java programming style, recommends that you “design and document for inheritance or else prohibit it.” This means all classes and methods that aren’t specifically intended to be overridden in subclasses ought to be explicitly marked as final. Kotlin follows the same philosophy. Whereas Java’s classes and methods are open by default, Kotlin’s are final by default.
Kotlin Open Keyword
Kotlin’s “design and document for inheritance or else prohibit it” philosophy is aimed at making code more robust and less error-prone. By default, classes and methods in Kotlin are final and cannot be inherited or overridden, which means that developers must explicitly declare a class or method as open in order to allow inheritance or overriding.
This approach differs from Java, where classes and methods are open by default, and must be explicitly marked as final to prohibit inheritance or overriding. While this default openness in Java allows for greater flexibility and extensibility, it can also lead to potential errors and security vulnerabilities if classes or methods are unintentionally overridden or inherited.
Kotlin’s approach is designed to encourage developers to carefully consider whether inheritance or overriding is necessary for a given class or method, and to document their intentions clearly. This can help prevent unintentional errors and make code more maintainable over time.
That being said, Kotlin recognizes that there are cases where inheritance and overriding are necessary or desirable. This is why the open keyword exists – to explicitly allow for classes and methods to be inherited or overridden when needed. By requiring developers to explicitly declare a class or method as open, Kotlin ensures that these features are used deliberately and with intention.
So, in summary, Kotlin’s approach to inheritance and overriding is designed to encourage careful consideration and documentation, while still allowing for these features when needed. The open keyword provides a way to explicitly allow for inheritance and overriding, while still maintaining Kotlin’s default “design and document for inheritance or else prohibit it” philosophy.
I hope this helps clarify why Kotlin chose to introduce the open keyword, despite its overall philosophy of limiting inheritance and overriding by default!
Kotlin is a modern, concise and powerful programming language that has gained a lot of popularity among developers in recent years. One of the features that makes Kotlin stand out is its powerful when expression, which is a more expressive version of the traditional switch statement in Java. In this article, we will dive deep into the power of when expressions in Kotlin.
Syntax of When Expressions in Kotlin
The syntax of the when expression is straightforward and intuitive. It consists of a keyword “when”, followed by a variable or expression in parentheses, and then a series of cases separated by commas. Each case is defined by a value or a range of values, followed by the arrow operator (->) and the block of code to execute. Finally, there is an optional else block, which executes when none of the cases match.
Kotlin
when (variable) { case1Value -> {// Code to execute when case1Value matches the variable } case2Value, case3Value -> {// Code to execute when case2Value or case3Value matches the variable }in case4Value..case6Value -> {// Code to execute when the variable is in the range from case4Value to case6Value }else-> {// Code to execute when none of the above cases match }}
Examples
Here are some simple examples that demonstrate the power of when expressions in Kotlin:
Matching on Types:
When expressions in kotlin can be used to match on types of objects. For example, the following code matches on a variable x and executes different blocks of code depending on whether x is an Int, a String, or a Boolean:
Kotlin
when (x) {is Int -> {// Code to execute when x is an integer }is String -> {// Code to execute when x is a string }is Boolean -> {// Code to execute when x is a boolean }else-> {// Code to execute when x is none of the above types }}
Matching on Values:
When expressions in kotlin can also be used to match on specific values. For example, the following code matches on a variable x and executes different blocks of code depending on whether x is 1, 2, 3 or none of the above:
Kotlin
when (x) {1-> {// Code to execute when x is 1 }2-> {// Code to execute when x is 2 }3-> {// Code to execute when x is 3 }else-> {// Code to execute when x is none of the above values }}
Matching on Ranges:
When expressions in kotlin can also match on ranges of values. For example, the following code matches on a variable x and executes a block of code if x is in the range from 1 to 10:
Kotlin
when (x) {in1..10-> {// Code to execute when x is in the range from 1 to 10 }else-> {// Code to execute when x is not in the range from 1 to 10 }}
Multiple Conditions:
When expressions in kotlin can match on multiple conditions. For example, the following code matches on a variable x and executes a block of code if x is greater than 10 and less than 20:
Kotlin
when (x) {in11..19-> {// Code to execute when x is in the range from 11 to 19 }else-> {// Code to execute when x is not in the range from 11 to 19 }}
Smart Casting:
When expressions in kotlin can be used to cast an object to a specific type. For example, the following code matches on a variable x and casts it to an Int if possible, then executes a block of code that uses the casted Int:
Kotlin
when (x) {is String -> {val length = x.length// Code to execute using the length of the string }is Int -> {valvalue = x// Code to execute using the integer value of x }else-> {// Code to execute when x is neither a String nor an Int }}
Returning Values:
When expressions in kotlin can return values, which can be useful in functional programming. For example, the following code matches on a variable x and returns a corresponding value depending on whether x is 1, 2, or none of the above:
Kotlin
val result = when (x) {1->"One"2->"Two"else->"Other"}
It’s worth noting that when expressions in kotlin are often used in conjunction with other Kotlin features, such as enum, extension functions, lambdas, and sealed classes, to provide even more powerful and expressive code.
Now let’s see some cool ways to deal with other Kotlin features
Using “when” to deal with enum classes:
Kotlin’s “enum” class is a powerful and flexible way to represent a fixed set of values. When expressions in kotlin can be particularly useful for dealing with enum classes, as they can handle each value in a concise and readable way. For example:
Here, the “when” expression takes in a value of type “Color” and matches each possible value of the enum with a corresponding string. This makes the code more concise and easier to read than a series of “if-else” statements.
Using “when” with arbitrary objects:
In addition to using “when” expressions with enum classes, Kotlin also allows developers to use “when” with arbitrary objects. This can be useful for handling multiple types of objects in a concise and readable way. For example:
Kotlin
fundescribeObject(obj: Any): String = when (obj) {is String ->"String with length ${obj.length}"is Int ->"Integer with value $obj"is Long ->"Long integer with value $obj"else->"Unknown object"}
Here, the “when” expression takes in an arbitrary object of type “Any” and matches it against each possible type using “is” checks. This allows the function to provide a concise and readable description of any object that is passed in.
Using “When” with extension functions:
Kotlin also allows developers to use “when” with extention functions in Kotlin, Here’s an example:
In this example, we define two extension functions called isEven and isOdd that can be called on integer values. We then define another extension function called classify that uses a “when” expression to classify integer values as “Even”, “Odd”, or “Unknown”. The “when” expression is called on the integer value using the this keyword.
Using “When” with Sealed Classes
Using “when” expressions with sealed classes is a powerful feature in Kotlin that allows for concise and type-safe pattern matching.
In this example, we define a sealed class called AppState, which represents the state of an app screen. It has three possible subclasses: Loading, Success, and Error. The Success subclass contains a list of Item objects, while the Error subclass contains an error message.
We then define a function called renderState that takes an AppState instance as a parameter and uses a “when” expression to pattern match on the possible subclasses. Depending on the subclass, it calls different functions to render the appropriate screen.
Using “when” expressions with sealed classes in this way allows for more readable and maintainable code, as the pattern matching is type-safe and localized to a single function. It also allows for easy extensibility of the AppState hierarchy, as new subclasses can be added to represent different states of the app screen.
Using “When” with Lambda
Here’s a simple example of using a “when” expression with a lambda in Kotlin to filter a list of numbers:
Kotlin
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)val evenNumbers = numbers.filter { number ->when (number % 2) {0->true// The number is even, so include it in the filtered listelse->false// The number is odd, so exclude it from the filtered list }}println(evenNumbers) // Prints: [2, 4, 6, 8, 10]
In this example, we start with a list of numbers and use the “filter” function to create a new list that only includes the even numbers. We pass a lambda expression to the “filter” function, which takes each number in the list and applies the “when” expression to determine if it should be included in the filtered list.
The “when” expression checks if the number is divisible by 2 (i.e., if it’s even) and returns true if it is, and false if it’s not. This lambda expression is concise and easy to read, and demonstrates how “when” expressions can be used in practical scenarios to create more expressive and flexible code.
Pattern Matching: To Regex or Custom Type
Here’s an example of how “when” expressions can be used for pattern matching:
In this example, we define a function called checkType that takes an arbitrary object as its input and uses a “when” expression to perform pattern matching on the object. The expression checks whether the object is an integer, a string, or a list of any type of element, and then prints a message indicating the type of the object.
Note that “when” expressions are not limited to simple type checks, but can also be used for more complex pattern-matching scenarios involving regular expressions or custom types. However, this example demonstrates the basic pattern-matching functionality of “when” expressions.
Lets see couple of examples of how “when” expressions can be used for more complex pattern matching scenarios in Kotlin.
1. Matching against regular expressions:
Kotlin
val input = "Hello123"val pattern = "d+".toRegex()val result = when (input) { pattern.matches(input) ->"Input matches pattern"else->"Input does not match pattern"}println(result)
In this example, we are using a “when” expression to match the input string against the regular expression pattern \\d+, which matches one or more digits. If the input matches the pattern, we print “Input matches pattern”. If it does not match the pattern, we print “Input does not match pattern”.
2. Matching against custom types:
Kotlin
sealedclassAnimal {objectDog : Animal()objectCat : Animal()objectFish : Animal()}fundescribeAnimal(animal: Animal) = when (animal) {is Animal.Dog ->"This is a dog."is Animal.Cat ->"This is a cat."is Animal.Fish ->"This is a fish."}val myPet = Animal.Dogprintln(describeAnimal(myPet))
In this example, we have a sealed class Animal with three objects: Dog, Cat, and Fish. We then define a function describeAnimal that takes in an Animal object and uses a “when” expression to match against the three possible cases of the sealed class. Depending on which case matches, the function returns a description of the animal.
Finally, we create an instance of Animal.Dog and pass it to describeAnimal, which prints “This is a dog.” to the console.
Smart casts: combining type checks and casts
You might have an idea about smart casts, as we saw in the previous smart casting example. In Kotlin, “smart casts” are a feature that allows developers to combine type checks and casts in a single operation. This can make code more concise and less error-prone. For example:
Kotlin
funprocessString(str: Any) {if (str is String) {println(str.toUpperCase()) }}
Here, the “is” check ensures that the variable “str” is of type “String”, and the subsequent call to “toUpperCase()” can be made without casting “str” explicitly.
This same functionality can be achieved using a “when” expression, like so:
Kotlin
funprocessString(str: Any) = when (str) {is String -> str.toUpperCase()else->""}
Here, the “when” expression combines the “is” check and the call to “toUpperCase()” in a single operation. If the variable “str” is not a string, the expression returns an empty string.
Refactoring: replacing “if” with “when”:
Kotlin’s “when” expression can be a useful tool for refactoring existing “if-else” code. In many cases, “if-else” statements can be replaced with a “when” expression, which can make the code more concise and easier to read. For example:
This can be refactored into a “when” expression like so:
Kotlin
fungetMessage(isValid: Boolean): String = when (isValid) {true->"Valid"false->"Invalid"}
Here, the “when” expression replaces the “if-else” statement, resulting in more concise and readable code.
Blocks as branches of “if” and “when”:
Kotlin’s “if” and “when” expressions can also be used to evaluate blocks of code, rather than just simple expressions. This can be useful for handling complex logic or multiple statements. For example:
Here, each branch of the “when” expression contains a block of code, allowing for more complex logic to be evaluated. This can make the code more readable and easier to maintain.
Real-world examples in Android Code
Let’s see some real-world examples of where when expressions in kotlin can be used in Android app code:
Handling different button clicks: In an app with multiple buttons, “when” expressions can be used to handle the click events for each button. For example:
Displaying different views: In a complex app with many different screens, “when” expressions can be used to display the appropriate view for each screen. For example:
Kotlin
when (screen) { Screen.Home -> {// Display the home screen view } Screen.Profile -> {// Display the profile screen view }// Handle other screen views}
Handling different data types: In an app that deals with different data types, “when” expressions can be used to handle each data type appropriately. For example:
Kotlin
when (data) {is String -> {// Handle string data type }is Int -> {// Handle integer data type }// Handle other data types}
Handling different API responses: In an app that communicates with an API, “when” expressions can be used to handle the different responses from the API. For example:
Kotlin
when (response) {is Success -> {// Handle successful API response }is Error -> {// Handle API error response }// Handle other API responses}
When expression in Extention Function: Here’s an example of using “when” expressions with extension functions in an Android app:
When expression in Lambda expression: Lambda expressions are often used in conjunction with “when” expressions in Android. Here’s an example:
Kotlin
val sharedPreferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE)val isDarkModeEnabled = sharedPreferences.getBoolean("isDarkModeEnabled", false)val statusBarColor = when { isDarkModeEnabled -> Color.BLACKelse-> Color.WHITE}window.statusBarColor = statusBarColor
Let’s see some benefits and limitations of “when” expressions in Kotlin:
Benefits:
Concise and expressive: “when” expressions can help make your code more concise and expressive, which can improve readability and maintainability.
More flexible than switch statements: “when” expressions are more flexible than traditional switch statements, as they can handle a wider range of conditions and types.
Smart casting: “when” expressions can be used to perform smart casting, which can help eliminate the need for explicit type casting in your code.
Works with enums and arbitrary objects: “when” expressions can be used with enums and arbitrary objects, which can help simplify your code.
Supports functional programming: “when” expressions can be used with lambda functions, which makes them well-suited to functional programming approaches.
Limitations:
Limited readability in complex scenarios: “when” expressions can become difficult to read and understand in more complex scenarios, especially when there are nested conditions or multiple actions associated with each condition.
No fall-through behavior: Unlike switch statements, “when” expressions do not support fall-through behavior. This means that you cannot execute multiple actions for a single condition without using additional code.
Limited to simple pattern matching: “when” expressions are limited to simple pattern matching, and cannot be used for more complex operations such as complex regular expressions.
Limited backward compatibility: “when” expressions were introduced in Kotlin 1.1 and are not available in earlier versions of the language.
In Kotlin, default parameters are a feature that allows developers to define default values for function parameters. This means that if a function is called with fewer arguments than expected, the default values will be used instead. Default parameters can help simplify function calls and reduce the amount of code you need to write.
Defining Default Parameters
To define default parameters in Kotlin, you simply specify a default value for a parameter in the function declaration. Here’s an example:
Kotlin
funsayHello(name: String = "softAai") {println("Hello, $name!")}sayHello() // prints "Hello, softAai!"sayHello("amol") // prints "Hello, amol!"
In this example, the sayHello function has a default parameter name with a default value of \"softAai\". When called with no arguments, the function will print \”Hello, softAai!\”. When called with an argument, such as \"amol\", the function will print \”Hello, amol!\”.
Using Default Parameters in Functions
Default parameters can be useful when writing functions that have many optional parameters or when you want to provide a default value for a parameter that is commonly used. Here’s an example of a function that uses default parameters to simplify its signature:
Kotlin
funsendResumeEmail(to: String, subject: String = "", body: String = "") {// send email with given parameters and attached resume }sendResumeEmail("[email protected]") // sends resume email with empty subject and bodysendResumeEmail("[email protected]", "Resume") // sends resume email with "Resume" as subject and empty bodysendResumeEmail("[email protected]", "Resume", "PFA!") // sends resume email with "Resume" as subject and "PFA!" as body
In this example, the sendResumeEmail function has two optional parameters, subject and body, with default values of \"\". This allows the function to be called with just a to parameter, which will send an email with an empty subject and body, or with both subject and body parameters, which will send a resume email with the specified subject and body.
Using Default Parameters with Named Parameters
Default parameters can also be used with named parameters to make the function call more readable and self-documenting. Here’s an example:
In this example, the calculateInterest function has three parameters, principal, rate, and years, with default values of 0.05 and 1, respectively. By using named parameters, we can specify only the parameters we care about and use the default values for the others. This makes the function call more readable and reduces the amount of boilerplate code needed.
Using Default Parameters with Extension Functions
Default parameters can also be used with extension functions in Kotlin. When defining an extension function with default parameters, you can specify default values for any optional parameters. Here’s an example:
In this example, we define an extension function on the String class called format. The format function takes three optional parameters: separator, prefix, and suffix. Each parameter has a default value, making them optional. The format function splits the string by commas and joins the resulting list with the specified separator, prefix, and suffix.
Using Default Parameters with Java Interoperability
Default parameters can also be used with Java interoperability in Kotlin. When a Kotlin function with default parameters is called from Java code, the default values are automatically generated as overloaded methods. Here’s an example:
In this example, we use the @JvmOverloads annotation to tell the Kotlin compiler to generate overloaded methods for each combination of parameters, including the default parameters. This allows Java code to call the greet function with either one or two parameters, using the default value for the second parameter if it is not specified.
Using Default Parameters in Function Overloading
Default parameters can also be used in function overloading in Kotlin. When defining overloaded functions with default parameters, you can specify different default values for each function. Here’s an example:
Kotlin
funmultiply(x: Int, y: Int = 1) = x * yfunmultiply(x: Double, y: Double = 1.0) = x * y
In this example, we define two overloaded functions called multiply. The first function takes two integers and multiplies them together, with a default value of 1 for the second parameter. The second function takes two doubles and multiplies them together, with a default value of 1.0 for the second parameter. This allows callers to use either function with different types of parameters, while still providing default values for the optional parameters.
One more thing to note about default parameters in Kotlin is that they can only be defined for parameters that come after all non-default parameters. In other words, if a function has a mix of parameters with default and non-default values, the non-default parameters must come first in the parameter list, followed by the parameters with default values.
For example, this is a valid function with default parameters in Kotlin:
In this example, the name parameter is a required parameter, while the greeting parameter has a default value of \”Hello\”. If we were to call the function without providing a value for greeting, the default value would be used:
Kotlin
sayHello("softAai") // Prints "Hello, softAai!"
However, if we were to define the function with the parameters in the opposite order, it would not be valid:
Kotlin
// This is not valid!funsayHello(greeting: String = "Hello", name: String) {println("$greeting, $name!")}
This is because the greeting parameter with the default value comes before the required name parameter. Defining default parameters in this way would result in a compilation error.
So in Kotlin, when defining a function with default parameters, the parameters with default values must come after all the parameters without default values in the function declaration. If this order is not followed, the code will not compile and the Kotlin compiler will generate an error message. This is a design decision made in the Kotlin language to prevent potential errors and to ensure clarity and consistency in the way functions with default parameters are defined.
Benefits of default parameters in Kotlin:
Default parameters allow developers to define function parameters with default values, which can be used if a value is not provided by the caller. This can simplify function calls by reducing the number of parameters that need to be specified.
Default parameters can help improve code readability, as developers can define a function with fewer parameters, making it easier to read and understand.
Default parameters can also simplify function overloading, as developers can define multiple versions of a function with different default parameter values, reducing the need for separate functions with different parameter lists.
Default parameters can help improve code maintenance, as developers can change the default parameter values in a function without having to modify all the callers of that function.
Limitations of default parameters in Kotlin:
One of the limitations of default parameters is that they can only be defined for function parameters, not for properties or other types of variables.
Default parameters can also make it more difficult to understand the behavior of a function, as callers may not be aware of the default parameter values and may not explicitly provide all necessary parameters.
Default parameters can also lead to ambiguous function calls if there are multiple functions with similar parameter lists and default values.
Default parameters can also have a negative impact on performance if a function is called with default parameter values frequently, as the function may need to execute additional logic to handle the default values.
Overall, default parameters in Kotlin can provide benefits in terms of code readability, maintenance, and simplifying function calls, but they should be used carefully and with consideration for their limitations.
Kotlin is a modern programming language that has become popular for its concise syntax, powerful features, and seamless interoperability with Java. One of the features that sets Kotlin apart from other languages is its support for named parameters. Named parameters provide developers with more flexibility and readability when working with functions and methods. In this article, we will cover all aspects of named parameters in Kotlin and provide examples to help you understand how they work.
What are named parameters?
Named parameters allow you to pass arguments to a function or method by specifying the parameter name along with its value. This provides greater clarity and reduces the chance of errors when calling functions with many parameters or when dealing with optional parameters.
For example, consider the following function in Kotlin:
fun createPerson(name: String, age: Int, address: String) { // implementation here }
To call this function in Kotlin, you would typically pass the arguments in the order that they are defined in the function signature:
This makes the code more readable and reduces the risk of passing the wrong values to the wrong parameters.
Kotlin introduced named parameters as a way to improve the readability and maintainability of code. In traditional programming languages, like Java, method parameters are passed in a specific order and it can sometimes be difficult to remember which parameter comes first, especially when the method has many parameters. Named parameters in Kotlin allow developers to specify the purpose of each parameter explicitly, making the code easier to understand and modify.
Named parameters also provide additional flexibility when calling functions or constructors. With named parameters, you can omit some parameters and use default values for them, while specifying values for only the parameters you care about. This can reduce the amount of boilerplate code needed and make the code more concise.
Another advantage of named parameters is that they make it easier to refactor code. When you add, remove, or reorder parameters in a function or constructor, you don’t need to worry about breaking any existing code that calls the function, as long as you use named parameters. This can save you time and effort when making changes to your code.
When using named parameters, it’s important to choose meaningful and descriptive names for the parameters.
Here is an example of using meaningful and descriptive names:
fun calculateBMI(weightInKg: Double, heightInCm: Double): Double { val heightInMeters = heightInCm / 100 return weightInKg / (heightInMeters * heightInMeters) }
In this example, we have defined a function called calculateBMI that takes two parameters: weightInKg and heightInCm. By using meaningful and descriptive names for the parameters, we can make it clear what each parameter represents and what units they are measured in. We are also using named parameters when calling the calculateBMI function to make the code more readable and self-documenting.
How To Use Named Parameters in Kotlin?
Let’s see different use cases where we use named parameters in Kotlin
1. Using named parameters with default values
Kotlin also allows you to define default parameter values for functions, which are used when a value is not provided by the caller. When combined with named parameters, this can make your code even more concise and expressive.
Consider the following function, which defines a default value for the address parameter:
fun createPerson(name: String, age: Int, address: String = \"Unknown\") { // implementation here }
With named parameters, you can call this function and only provide the non-default parameters:
createPerson(name = \"amol pawar\", age = 25)
In this example, the address parameter will default to \"Unknown\".
2. Using named parameters with both functions and constructors
Named parameters can be used with both functions and constructors in Kotlin. Here is an example of using named parameters with a constructor:
class Person(val name: String, val age: Int, val gender: String) { // ... }
val person = Person(name = \"Amol\", age = 30, gender = \"male\")
In this example, we are creating an instance of the Person class using named parameters. By using named parameters, we can explicitly state the purpose of each parameter and make the code more readable.
Let’s see another example where we use default parameter values for one or more parameters.
fun printMessage(message: String, count: Int = 1) { repeat(count) { println(message) } }
printMessage(\"Hello\") // prints \"Hello\" once printMessage(\"World\", 3) // prints \"World\" three times
In this example, we have defined a function called printMessage that takes two parameters: message and count. The count parameter has a default value of 1, which means that if it is not specified when calling the function, it will default to 1.
One more use case we need to consider here is, we can use named parameters to specify only some of the parameters for a function or constructor, and omit others.
greet(\"Amol\") // prints \"Hello, Amol!\" greet(\"softAai\", \"Hi\") // prints \"Hi, softAai!\"
In this example, we have defined a function called greet that takes two parameters: name and greeting. The greeting parameter has a default value of \"Hello\", which means that if it is not specified when calling the function, it will default to \"Hello\". By using named parameters, we can omit the greeting parameter and use the default value.
3. Using named parameters with both positional and non-positional arguments
In Kotlin, you can use named arguments with both positional and non-positional arguments. Here is an example of using named arguments with both types of arguments:
fun printValues(a: Int, b: Int, c: Int) { println(\"a = $a, b = $b, c = $c\") }
printValues(a = 1, b = 2, c = 3) // named arguments printValues(1, c = 3, b = 2) // positional and named arguments
In this example, we have defined a function called printValues that takes three parameters: a, b, and c. We can call this function using named arguments or a combination of positional and named arguments.
4. Using named parameters with varargs
Kotlin also supports varargs, which allows you to pass an arbitrary number of arguments to a function or method. When using named parameters with varargs, you can specify the name of the parameter and then provide a comma-separated list of values.
Consider the following function, which accepts a vararg of integers:
fun sum(vararg numbers: Int): Int { return numbers.sum() }
To call this function with named parameters, you would specify the parameter name followed by a comma-separated list of values:
val result = sum(numbers = 1, 2, 3, 4, 5)
In this example, the numbers parameter is assigned the values 1, 2, 3, 4, 5.
5. Using named parameters with extension functions
Kotlin also allows you to use named parameters with extension functions, which are functions that can be called as if they were methods of an object.
Consider the following extension function, which adds an exclamation point to a string:
fun String.addExclamationPoint(times: Int = 1): String { return this + \"!\".repeat(times) }
To call this extension function with named parameters, you would specify the parameter name followed by its value:
val result = \"Hello\".addExclamationPoint(times = 3)
In this example, the times parameter is assigned the value 3.
Named Parameters & Interoperability with Java
Kotlin is designed to be highly interoperable with Java, which means that Kotlin code can be used in Java projects and vice versa. However, when using named parameters in Kotlin, there are some considerations to keep in mind to ensure interoperability with Java code.
When calling a Kotlin function with named parameters from Java, the named parameters are not supported and instead, the arguments must be passed in the order that they are defined in the function signature. For example, consider the following Kotlin function with named parameters:
fun calculateArea(length: Int, width: Int, units: String = \"square units\"): String { val area = length * width return \"The area is $area $units\" }
To call this function from Java, the named parameters cannot be used, so the arguments must be passed in the correct order:
String result = MyKotlinClass.INSTANCE.calculateArea(10, 20, \"square meters\");
In this example, the arguments are passed in the order that they are defined in the Kotlin function signature, with the default value for the units parameter overridden by the value \"square meters\".
To make the Kotlin function more interoperable with Java code, it’s a good idea to define overloaded versions of the function that take only the required parameters in the correct order. For example:
In this example, the @JvmOverloads annotation is used to generate overloaded versions of the function that take only the required parameters, with the default value for the units parameter used. These overloaded functions can be called from Java code without having to use named parameters.
Advantages of Named Parameters in Kotlin:
Increased readability: Named parameters can make code more readable and easier to understand by explicitly stating the purpose of each parameter.
Improved code maintenance: Named parameters can reduce errors that might arise when code is being maintained or modified because changes to the code can be made without having to worry about the order of parameters.
Flexibility: Named parameters can be used with default values, which can make code more concise and expressive.
Better documentation: Named parameters can help to document the code and make it more self-explanatory.
Easier to use with overloaded functions:Named parameters can make it easier to call overloaded functions by providing a clear indication of which parameters are being passed to the function.
Disadvantages of Named Parameters in Kotlin:
Increased verbosity: Using named parameters can make code more verbose, which can make it harder to read and understand.
Potential performance impact: Using named parameters can have a small performance impact due to the additional processing required to match the parameter names to their corresponding values.
Potential confusion with similar parameter names: Named parameters can potentially cause confusion if the names of parameters are similar, which can make it harder to identify which parameter is being referred to.
Potential for misuse: Named parameters can be misused if developers use them excessively or incorrectly, which can lead to code that is harder to read and maintain.
Incompatibility with some Java code:Named parameters are not supported by Java, so when calling Kotlin code from Java, named parameters cannot be used. This can cause issues if Kotlin code is being integrated with existing Java codebases.
Summary
Overall, named parameters are a powerful feature in Kotlin that can make code more readable, maintainable, and expressive. By using them judiciously and following best practices, you can take advantage of the benefits of named parameters in your Kotlin code.
In Kotlin, a side effect is a change in the state of a program that occurs outside of the current function. A side effect can be caused by modifying a variable outside of a local scope(don’t worry we will look in detail), modifying the state of a database, making a network request, or updating a file. Side effects are considered impure because they introduce external dependencies and can cause unpredictable behavior.
Kotlin is a multi-paradigm programming language, which means that it supports both functional and imperative programming styles. In functional programming, a pure function is a function that has no side effects and produces the same output given the same input. Pure functions are predictable and easy to reason about, which makes them ideal for functional programming. In Kotlin, side effects are often managed using techniques such as monads or pure functional state.
A monad is a design pattern that is commonly used in functional programming to manage side effects. A monad is a data type that wraps a value and provides a way to chain together operations that can have side effects, while still maintaining referential transparency.
In other words, a monad is a way to encapsulate side effects and provide a functional interface for working with them. The key idea behind monads is that they provide a way to abstract away the complexity of side effects and provide a simple, composable interface for working with them.
What does mean by outside of local scope?
In programming, a function’s local scope refers to the area of the code where the function is defined. This includes any variables or parameters that are passed into the function as arguments. Any changes made to these variables or parameters within the function are considered to be within the function’s local scope.
For example, consider the following Kotlin function:
Kotlin
funaddTwoNumbers(a: Int, b: Int): Int {val sum = a + breturn sum}
In this function, the local scope includes the parameters a and b, as well as the variable sum. Any changes made to these variables or parameters within the function are considered to be within the local scope.
However, a function can also interact with variables or resources outside of its local scope. For example:
Kotlin
val x = 0funincrementX() { x++}
In this case, the incrementX function modifies the value of the variable x, which is defined outside of the function’s local scope. Any changes made to x within the function are considered to be outside of its local scope, and are therefore side effects.
Similarly, a function that reads from or writes to a file, sends a network request, or interacts with a database is considered to have side effects, because it is changing the state of an external resource outside of its local scope.
Managing Side Effects in Kotlin
Managing side effects in Kotlin requires careful attention to how external dependencies are accessed and modified. One approach is to use functional programming techniques such as pure functions or monads to isolate side effects from the rest of the code. Another approach is to use the suspend keyword to manage asynchronous side effects in a coroutine scope.
Pure Functions in Kotlin
Pure functions in Kotlin are functions that have no side effects and always return the same output given the same input. Pure functions are easy to reason about and test, since they only depend on their input parameters and do not modify any external state.
Here’s an example of a pure function in Kotlin:
Kotlin
funadd(a: Int, b: Int): Int {return a + b}
In this example, the add function takes two integers as input and returns their sum. The function has no side effects and always returns the same output given the same input. This makes the function pure and predictable.
In simple terms, if a function satisfies the below conditions, we can say it’s a pure function.
The function must always return a value.
It must not throw any exceptions or errors.
It must not mutate or change anything outside the scope of the function, and any changes within the function must also not be visible outside its scope.
It should not modify or change its argument.
For a given set of arguments, it should always return the same value.
A function that does not satisfy the above conditions is called an impure function (We will look at its definition and examples later here in this article)
Based on these conditions, let’s analyze and classify the functions as pure and impure:
1. Pure function without conditions:
Kotlin
funmultiply(a: Int, b: Int): Int {return a * b}
Returns value: ✅ (The function returns the product of a and b, an integer.)
No exceptions or errors: ✅ (There are no exceptions or errors in the function.)
No mutation of argument or external dependencies: ✅ (The function doesn’t modify any arguments or external variables.)
Always returns the same value for the same input: ✅ (For a given set of a and b, the function will always return the same result.)
2. Pure function with conditions:
Kotlin
fungetPositiveNumber(number: Int): Int {returnif (number >= 0) number else0}
Returns value: ✅ (The function returns an integer, either number or 0 based on the condition.)
No exceptions or errors: ✅ (There are no exceptions or errors in the function.)
No mutation of argument or external dependencies: ✅ (The function doesn’t modify any arguments or external variables.)
Always returns the same value for the same input: ✅ (For a given value of number, the function will always return the same result.)
3. Pure function with immutable data structures:
Kotlin
funappendElementToList(list: List<Int>, element: Int): List<Int> {return list + element}
Returns value: ✅ (The function returns a new list by appending element to the original list.)
No exceptions or errors: ✅ (There are no exceptions or errors in the function.)
No mutation of argument or external dependencies: ✅ (The function doesn’t modify any arguments or external variables.)
Always returns the same value for the same input: ✅ (For the same list and element, the function will always return the same resulting list.)
4. Pure function using Kotlin Standard Library functions:
Returns value: ❌ (The function doesn’t have a return type, and it doesn’t return any value.)
No exceptions or errors: ✅ (There are no exceptions or errors in the function.)
No mutation of argument or external dependencies: ❌ (The function modifies the external variable globalCounter, causing a side effect.)
Always returns the same value for the same input: N/A (The function doesn’t return any value, so this condition is not applicable.)
6. Impure function with changing results:
Kotlin
fungetRandomNumber(): Int {return (1..100).random()}
Returns value: ✅ (The function returns an integer, a random number between 1 and 100.)
No exceptions or errors: ✅ (There are no exceptions or errors in the function.)
No mutation of argument or external dependencies: ✅ (The function doesn’t modify any arguments or external variables.)
Always returns the same value for the same input: ❌ (The function returns different random values on each call, making it impure.)
7. Impure function with exception:
Kotlin
fundivide(a: Int, b: Int): Int {if (b == 0) throwIllegalArgumentException("Cannot divide by zero.")return a / b}
Returns value: ✅ (The function returns the result of the division a / b if b is not zero.)
No exceptions or errors: ❌ (The function throws an exception when b is zero, making it impure.)
No mutation of argument or external dependencies: ✅ (The function doesn’t modify any arguments or external variables.)
Always returns the same value for the same input: ✅ (For the same a and b (excluding the case where b is zero), the function will always return the same result.)
8. Impure function with external dependency:
Kotlin
funfetchUserData(userId: String): User {// Code to fetch user data from an external service/database// and return the user object.}
Returns value: ✅ (The function is expected to return a User object fetched from an external service/database.)
No exceptions or errors: ❌ (The function may throw exceptions if the external service is down or there’s a data retrieval issue.)
No mutation of argument or external dependencies: ❌ (The function interacts with an external service/database, making it impure.)
Always returns the same value for the same input: N/A (The function’s behavior depends on the external service/database, so this condition is not applicable.)
9. Impure function modifying mutable data:
Kotlin
funincrementListItems(list: MutableList<Int>) {for (i in0 until list.size) { list[i]++ }}
Returns value: ❌ (The function doesn’t have a return type, and it doesn’t return any value.)
No exceptions or errors: ✅ (There are no exceptions or errors in the function.)
No mutation of argument or external dependencies: ❌ (The function modifies the list by incrementing its elements, causing a side effect.)
Always returns the same value for the same input: N/A (The function doesn’t return any value, so this condition is not applicable.)
Impure Functions in Kotlin
Impure functions in Kotlin are functions that have side effects and modify external state. Impure functions can be more difficult to reason about and test, since they can have unpredictable behavior depending on the current state of the program.
Here’s an example of an impure function in Kotlin:
Kotlin
var counter = 0funincrementCounter() { counter++println("Counter is now $counter")}
In this example, the incrementCounter function modifies the value of the counter variable outside of the local scope of the function. This introduces a side effect, since the function modifies external state. The function also prints the current value of the counter variable, which is another side effect.
Let’s see a few more examples of side effects in Kotlin
This function writes the input content to a file specified by filename. Writing to a file is an example of a side effect because it modifies the state of an external resource outside of its local scope.
2. Changing the Value of a Variable
Kotlin
var x = 0funincrement() { x++}
This function increments the value of a global variable x by 1. Modifying the value of a variable outside of the local scope of a function is an example of a side effect.
This function sends an HTTP request to retrieve JSON data from a specified url. Sending an HTTP request is an example of a side effect because it interacts with an external resource outside of its local scope.
Kotlin Coroutines
Kotlin coroutines provide a way to manage asynchronous side effects in Kotlin. Coroutines are lightweight threads that allow you to write asynchronous code in a synchronous style. Coroutines allow you to suspend the execution of a function until a result is available, without blocking the main thread.
Here’s an example of using coroutines to manage a network request in Kotlin:
In this example, we’re using the withContext function to execute a network request in a coroutine scope. The withContext function takes a coroutine context and a suspendable block of code, and suspends the coroutine until the block completes.
Dispatchers.IO is a coroutine dispatcher that is optimized for I/O operations such as network requests. By using withContext, we’re able to manage the side effect of a network request without blocking the main thread.
Here’s an example of using a monad in Kotlin to manage a network request:
In this example, we’re using the Either monad from the Arrow library to manage a network request in a functional way. Either monad is used to represent a computation that can return either a value or an error. The catch function is used to catch any exceptions that may occur during the network request and return an error value.
By using a monad, we’re able to encapsulate the side effects of the network request and provide a functional interface for working with the result. This makes it easier to reason about and test the code and also makes it easier to compose multiple side-effecting computations together.
How to Avoid Side Effects?
Consider the following simple function that takes a list of integers, adds a new value to it, and returns the updated list:
This function modifies the state of the input list by adding a new element to it. This is an example of a side effect, as the function changes the state of an object outside of its local scope.
To avoid side effects, you could modify the function to create a new list instead of modifying the input list directly:
Kotlin
funaddValue(list: List<Int>, value: Int): List<Int> {return list + value}
This function takes a read-only list as input and returns a new list that includes the input value. The input list is not modified, and there are no side effects. as we know side effects occur when a function modifies state outside of its local scope. By using functional programming techniques, you can reduce side effects and make your code more maintainable and predictable.
What about Jetpack Compose Side Effects?
The concept of side effects is present in both functional programming (Kotlin) and Jetpack Compose, but there are some differences in how they are handled.
In functional programming, a pure function is a function that has no side effects and produces the same output given the same input. Pure functions are predictable and easy to reason about, which makes them ideal for functional programming. Side effects in functional programming are usually avoided, but when necessary, they are managed using techniques such as monads or pure functional state.
In Jetpack Compose, on the other hand, side effects are a common occurrence due to the nature of UI programming, where interactions with external resources such as databases, network requests, or sensors are often necessary. Jetpack Compose provides a way to manage side effects using the LaunchedEffect and SideEffect APIs, which allow you to execute side effects in a controlled and predictable manner.
Here’s an example:
Kotlin
@ComposablefunMyComposable() {val context = LocalContext.currentvar myData byremember { mutableStateOf("") }LaunchedEffect(Unit) {val result = fetchDataFromDatabase(context) myData = result }Text(text = myData)}suspendfunfetchDataFromDatabase(context: Context): String {// perform some asynchronous operation to fetch data from a databasereturn"Hello, softAai!"}
In this example, we’re using LaunchedEffect to fetch data from a database asynchronously and update the UI when the data is available. LaunchedEffect is a composable function that runs a side effect in a coroutine scope. In this case, we’re using a suspend function fetchDataFromDatabase to fetch data from a database and update the UI with the result.
The remember function is used to store the current state of myData, which is initially an empty string. Once the data is fetched from the database, we update the state of myData with the result.
By using LaunchedEffect, we’re able to manage the asynchronous nature of the side effect and update the UI when the data is available. This helps to keep our composable functions pure and predictable, while still allowing us to interact with external resources.
Another important distinction is that functional programming emphasizes the avoidance of side effects, while Jetpack Compose acknowledges that side effects are often necessary in UI programming and provides tools to manage them. However, both approaches share the goal of maintaining predictability and reducing complexity in software development.
Conclusion
Managing side effects in Kotlin requires careful attention to how external dependencies are accessed and modified. Using functional programming techniques such as pure functions and monads, or managing asynchronous side effects with coroutines, can help isolate side effects from the rest of the code and make it easier to reason about and test.
By understanding how side effects work in Kotlin, you can write more predictable and maintainable code that is less prone to bugs and errors. It’s important to use best practices for managing side effects and to understand how different approaches can affect the behavior of your program.