Introduction
Greetings, Swift Community! In our previous article, we delved into Swift PM libraries within Android Studio projects. Today, we will continue our series on developing Android applications by harnessing the power of Swift PM libraries.
As we already know, Android app development has long relied on Java and Kotlin. But for Swift-savvy developers, known for its expressiveness and safety, the allure of using it in Android is strong. Until recently, running Swift on Android posed a challenge. Thankfully, the SCADE toolchain now solves this, bringing Swift to Android.
In this article, we will develop a simple Android application to implement recyclerview to display the list of Github followers using Github API & Swift PM library.
GitHub Code: https://github.com/6vedant/SwiftAndroidGithubApiExample
Setup Android Project with Swift PM library
Let’s start Android Studio, and create a new project, selecting an appropriate template that suits your application needs. In this guide, we’ll use the Empty Views Activity template as our project’s foundation. Once you’ve chosen the template, click “Next.”
On the second page, set project details like the name (SwiftAndroidExample), package name (com.example.myapplication
), and choose Java as the primary language for the project.
To initialize a new Swift Package Manager (SPM) project within the app/src/main/swift subdirectory, execute the following commands in the terminal:
cd SwiftAndroidExample/app/src/main/swift
swift package init --name SwiftAndroidExample
Add it build.gradle
implementation fileTree(dir: 'lib', include: ['*.jar'])
Add it in build.gradle
at end
task buildSwiftProject(type: Exec) {
commandLine '/Applications/Scade.app/Contents/PlugIns/ScadeSDK.plugin/Contents/Resources/Libraries/ScadeSDK/bin/scd',
'spm-archive', '--type', 'android',
'--path', 'src/main/swift',
'--platform', 'android-arm64-v8a',
'--platform', 'android-x86_64',
'--android-ndk', '~/Library/Android/sdk/ndk/25.2.9519653'
}
tasks.whenTaskAdded { task ->
if (task.name == 'assembleDebug' || task.name == 'assembleRelease') {
task.dependsOn buildSwiftProject
}
}
This code creates a special task called “buildSwiftProject” in Gradle. This task uses a tool called scd
(part of SCADE) to build the Swift project for Android on two different types of devices (ARM64 and x86_64). It needs the Android NDK to do this, and you should make sure to provide the correct NDK location, which might vary depending on the NDK version you have. If you installed SCADE in a custom location, replace /Applications/Scade.app
with the actual path on your computer. This path points to where the scd
tool is stored within the SCADE IDE application.
Give manifest permission
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication">
<!-- Other manifest entries -->
<!-- Add INTERNET permission to allow network access -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Add ACCESS_NETWORK_STATE permission to check network connectivity -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Other manifest entries -->
</manifest>
Create Swift Methods in Activity
Now we need to initialize the SwiftFoundation
so that it can call the Swift runtime environment and load the JNI library to execute the Swift methods and pass the results from Swift methods to the JVM environment using the JNI bridge.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
// initializing swift runtime.
// The first argument is a pointer to java context (activity in this case).
// The second argument should always be false.
org.swift.swiftfoundation.SwiftFoundation.Initialize(this, false);
} catch (Exception err) {
android.util.Log.e("SwiftAndroidExample", "Can't initialize swift foundation: " + err.toString());
}
// loading dynamic library containing swift code
System.loadLibrary("SwiftAndroidExample");
}
As the next step, let’s declare the Swift method in Activity which will be implemented in Swift class. The Android activity will consume the result of the Swift method and will use the result to display in the activity’s UI.
private native void loadData(String url);
Now let’s call the loadData()
method in onCreate()
method of Activity. It will call the implementation of the loadData() method in Swift class.
Implement the API Call in the Swift class
As soon as loadData()
the method is called, it will call the Java equivalent native Swift method defined in the Swift class. So let’s define it using activity class name.
// NOTE: Use @_silgen_name attribute to set native name for a function called from Java
@_silgen_name("Java_com_example_swiftandroidexample_MainActivity_loadData")
public func MainActivity_loadData(env: UnsafeMutablePointer<JNIEnv>, activity: JavaObject, javaUrl: JavaString) {
// Create JObject wrapper for activity object
let mainActivity = JObject(activity)
// Convert the Java string to a Swift string
let str = String.fromJavaObject(javaUrl)
// Start the data download asynchronously in the main actor
Task {
await downloadData(activity: mainActivity, url: str)
}
}
In this method, we will create a Java object wrapper for the activity instance which will convert the Java string to a Swift string using fromjavaObject
the method. Finally, we will call an asynchronous network call defined in another method downloadData()
. It accepts the activity instance and the API base URL which was called from onCreate
of Activity.
import Foundation
import Dispatch
import Java
// Downloads data from specified URL. Executes callback in main activity after download is finished.
@MainActor
public func downloadData(activity: JObject, url: String) async {
var request = URLRequest(url: URL(string: url)!,timeoutInterval: Double.infinity)
request.addValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
request.addValue("Bearer XXXXXXX", forHTTPHeaderField: "Authorization")
request.addValue("2022-11-28", forHTTPHeaderField: "X-GitHub-Api-Version")
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print(String(describing: error))
activity.call(method: "onDataLoaded", "Error")
return
}
let dataStr = (String(data: data, encoding: .utf8)!)
activity.call(method: "onDataLoaded", dataStr)
}
task.resume()
}
In downloadData()
method we will make a network call to the Github service to fetch the followers of a GitHub user. We will pass the API authorization token and other request parameters. Using the URLSession class to make the network call, it will return the data and response as callback parameters.
As the next step, using the activity instance we will call back the Java method of onDataLoaded and pass the data equivalent string as input parameter.
public void onDataLoaded(String data) {
// use data result called from Swift class
}
Build UI for Recyclerview
Let’s build the simple UI XML layouts for activity_main.xml
to load the recycler view in activity. We will use Relativelayout to display recyclerview and a progress-bar to display till data is loaded into UI.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_below="@+id/followers"
android:id="@+id/container"
android:layout_margin="18dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true" />
</RelativeLayout>
As the next step, we need to build Ui for the generic recycler item for displaying the list of followers.
It uses LinearLayout to display a card layout containing the image of follower user and the GitHub ID.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_margin="4dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="2dp"
app:cardBackgroundColor="@color/white"
app:cardCornerRadius="2dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/imageFollowerIV"
android:layout_width="140dp"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:background="@color/white" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/githubUserNameTV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="4dp"
android:textStyle="bold"
android:text="Github User Name"
android:textColor="@color/black"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
Build Adapter for Recyclerview
The adapter class is very important for loading the Recyclerview. We need to first define the data model class to contain the follower data object. Let’s create GithubFollowerModel
that will contain the imageUr
l and userName
of Github follower user.
public class GithubFollowerModel implements Serializable {
private String imageUrl;
private String userName;
public GithubFollowerModel(String imageUrl, String userName, String userID) {
this.imageUrl = imageUrl;
this.userName = userName;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
As the next step, we will create the ViewHolder class that will inflate the recycler item layout using onCreateViewHolder()
.
@NonNull
@Override
public GithubFollowerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.follower_recycler_item, parent, false);
return new GithubFollowerViewHolder(view);
}
The ViewHolder instance will be used to display the image and Github userID in onBindViewHolder
the method. Here, we use Picasso library to load image from image url in Android.
@Override
public void onBindViewHolder(@NonNull GithubFollowerViewHolder holder, int position) {
GithubFollowerModel currGithubFollower = githubFollowerModels.get(position);
holder.githubUserNameTV.setText(currGithubFollower.getUserName());
// load the github profile image of follower
if(currGithubFollower.getImageUrl() != null) {
Picasso.get().load(Uri.parse(currGithubFollower.getImageUrl())).into(holder.githubUserImageIV);
}
}
Finally, Call the Adapter in onDataLoaded the method
We will need to call the ReyclerView adapter on the main thread and attach the adapter to the Recyclerview UI element.
// access recyclerview on main thread instance
Handler mainHandler = new Handler(getMainLooper());
Runnable myRunnable = new Runnable() {
@Override
public void run() {
followersHeadingTV.setText("Followers: ("+githubFollowerModels.size()+")");
if (githubFollowerModels.size() == 0) {
progressBar.setVisibility(View.GONE);
noFollowersTV.setVisibility(View.VISIBLE);
} else {
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(MainActivity.this);
recyclerView.setLayoutManager(linearLayoutManager);
GithubFollowerRecyclerAdapter githubFollowerRecyclerAdapter = new GithubFollowerRecyclerAdapter(MainActivity.this, githubFollowerModels);
recyclerView.setAdapter(githubFollowerRecyclerAdapter);
progressBar.setVisibility(View.GONE);
}
}
};
mainHandler.post(myRunnable);
In this method, we are using the main thread operation to declare the ReycyclerView adapter instance and set it to the recyclerview. Also, the progress bar is used to display till data is loaded and displayed into the UI.
Now Run and Test the App
We have completed our development and it is now time to run the app and check if the Swift method is able to make API calls and send the data back to Activity. Please make sure the emulator or physical device is running. Click on the Run button.
It was really interesting to create a Recyclerview Swift app in Android for Swift Developers who want to try Swift on Android Studio 🎉. It is really cool. You can now easily integrate the Swift runtime into your Android Studio application projects 😊.
Leave a Reply