Implement RecyclerView using Swift PM libraries

Implement RecyclerView using Swift PM libraries

Implement Recyclerview to display list of Github followers using Github API & Swift PM library.

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: Swift-Android-Github-API-Example

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

Image description

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.

Project Settings for Android 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 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 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 lets 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 URLSession 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.

<?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 imageUrl 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 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.

// 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 have completed our 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 Activity. Please make sure the emulator or physical device is running. Click on the Run button.

Image description

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