Implement RecyclerView using Swift PM libraries

--

Update: Great news! We have released a new tool exclusively for seamless Swift integration on Android. Introducing ‘Swift for Android’ — the complete solution to execute Swift code within your Android Studio projects. Enhance your development experience with the power of Swift in your Android projects. https://www.swift-android.com/

Introduction

Greetings, SCADE 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 list of Github followers using Github API & Swift PM library.

Github Code: https://github.com/scade-platform/FusionExamples/tree/main/SwiftAndroidGithubApiExample

Prerequisite: https://medium.com/@SCADE/using-swift-pm-libraries-in-the-android-studio-projects-7cef47c300bf

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.”

Choose Empty Views Activity

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.

Define New Project Settings

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

The swift package init command will set up a new SPM project named SwiftAndroidExample inside the swift subdirectory, allowing you to manage Swift dependencies and build your Swift code as a separate module within your Android Studio project.

At the end of the app/build.gradle file, add the following code:

implementation fileTree(dir: 'lib', include: ['*.jar'])

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 adds a custom Gradle task, buildSwiftProject, that will execute the scd utility (SCADE command-line tool) to build the Swift project.

Give manifest permission

These permissions enable Swift’s network functions to work correctly. Add the following lines to your AndroidManifest.xml file:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication">
<!-- 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 JVM environment using the JNI bridge. In onCreate() method of activity, we will initialize the Swift runtime.

@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 Swift method and will use the result to display in 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 Swift class

As soon as loadData() method is called, it will call the Java equivalent native Swift method defined in 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 Java object wrapper for the activity instance which will convert the Java string to a Swift string using fromJavaObject method. Finally, we will call an asynchronous network call defined in another method downloadData(). It accepts 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 network call to Github service to fetch the followers of a Github user. We will pass the API authorization token and other request parameters. Using the URLSessionURLSession class to make 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 RecyclerView 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.

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 imageUrl and userName of Github follower user.

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() 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 method

We will need to call the ReyclerView adapter on the main thread and attach the adapter to the RecyclerView ui element.

// inside onDataLoadedMethod
//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, progress bar is used to display till data is loaded and displayed into the UI.

Now Run and Test the App

We are done with the development and it is now time to run the app and check if Swift method is able to make API call and send the data back to the Activity. Please make sure the emulator or physical device is running.

RecyclerView using Swift in Android Studio

It was really interesting to create a RecyclerView Swift app in Android 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 😊.

--

--

Cross Platform App Development with Apple Swift

We use Medium to share our journey of creating what we hope will become the #1 next generation platform for native, cross platform app development www.scade.io