How to use the OpenCombine framework with Android Studio

How to use the OpenCombine framework with Android Studio

Use Swift's Reactive Programming OpenCombine framework in Android Studio!

Introduction

📱Namaste Mobile Developers Community 🙏! In the ever-evolving software development landscape, the demand for cross-platform solutions is more pronounced than ever. Developers seek tools that enable them to transfer their skills and codebases between different platforms seamlessly. When it comes to reactive programming in Swift, Apple's Combine framework stands as a robust solution. However, what if you want to extend the power of Combine to Android development?

Enter OpenCombine – an open-source implementation of Combine that opens the door to cross-platform reactive programming. With its declarative syntax, support for Combine-like features, and cross-platform compatibility, OpenCombine stands as a key player in the realm of reactive programming for Swift developers.

In the ongoing series of Swift for Android articles, we will delve into the intricacies of using OpenCombine with Android Studio, empowering developers to harness the magic of reactive programming on both Swift and Android. We will build a simple Task Management app where from Android’s activity the tasks can be created or deleted using Swift’s OpenCombine framework. The OpenCombine processes the tasks-data over time and updates the Android’s UI(Recyclerview) with the latest data.

Prerequisite

If you haven’t installed the SCADE IDE, download the SCADE IDE and install it on your macOS system. The only prerequisite for SCADE is the Swift language (at least basics). Also, please ensure that Android Studio with an Android emulator or physical device is running to test the Android application.

To know more about how to add Swift to existing Android projects, please go through these articles:

  1. Display list of GitHub followers using Github API & Swift PM library - https://medium.com/@SCADE/implement-recyclerview-using-swift-pm-libraries-eacc1efd48af

  2. Using Swift PM libraries in the Android Studio projects - https://medium.com/@SCADE/using-swift-pm-libraries-in-the-android-studio-projects-7cef47c300bf

Source Code of App

You can directly jump to the source code of the Task Management App using OpenCombine. drive.google.com/file/d/1Eq12boTsBkUpg8VYQP..

What is OpenCombine?

OpenCombine is an open-source implementation of the Combine framework, which is designed for reactive programming in Swift. It enables developers to work with asynchronous and event-driven code in a more declarative and concise manner. Combine is particularly powerful in the context of SwiftUI, where changes in data trigger automatic updates to the user interface. But it also works perfectly with the Android Studio Swift toolchain.

Key Features of OpenCombine:

  1. Publishers and Subscribers: Open Source Combine framework revolves around the concepts of publishers that emit values and subscribers that receive and react to those values.

  2. Operators: Combine provides a set of operators that allow developers to transform, filter, and combine streams of values.

  3. Error Handling: Combine includes mechanisms for handling errors in a reactive way, making it well-suited for handling asynchronous tasks.

  4. Declarative Syntax: With Combine, developers can express complex asynchronous logic in a more declarative manner, making the code more readable and maintainable.

Integrate OpenCombine using SPM

To seamlessly integrate OpenCombine into your Swift project, we can add the dependency using Swift Package Manager (SPM) by modifying the Package.swift file.

dependencies: [
   .package(url: "https://github.com/scade-platform/swift-java.git", branch: "main"),
   .package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.10.0")
],

These declarations ensure that the necessary frameworks are included, fostering a unified development environment that harnesses the combined power of Swift and Java for cross-platform scenarios.

.target(
   name: "SwiftAndroidExample",
   dependencies: [
           .product(name: "Java", package: "swift-java"),
           .product(name: "OpenCombine", package: "OpenCombine"),
   ]),

Create the Task-App's Backend in Swift

Before diving into creating and deleting functionality for Tasks, it's imperative to set the groundwork by importing the necessary packages.

import Dispatch
import Foundation
import Java
import OpenCombine

The Swift OpenCombine code encapsulates task management logic, featuring a singleton TaskListViewModel with reactive capabilities through a tasksPublisher. Designed for a cross-platform app, this backend seamlessly integrates with an Android UI, allowing consistent task handling across Swift and Android platforms. The use of shared business logic ensures a unified approach to task management in the application.

  1. Task Struct:

    • There's a Task struct representing a model for a task with two properties: id of type Int and title of type String. The struct conforms to the Identifiable protocol.
  2. TaskListViewModel Class:

    • TaskListViewModel is a class responsible for managing tasks.

    • It's a singleton class with a static instance accessible through viewModel.

    • The initializer is marked as private, ensuring that instances of this class can only be created from within the class itself.

  3. Tasks Array:

    • The class contains a tasks property, which is an array of Task objects. This array serves as the storage for the tasks managed by the view model.
  4. Tasks Publisher:

    • There's a tasksPublisher property, which is an instance of PassthroughSubject<[Task], Never>. This subject acts as a publisher for changes in the tasks array.
  5. addTask Method:

    • The addTask method adds a new task to the tasks array, prints a message indicating that the task has been added, and then sends the updated array through the tasksPublisher.
  6. removeTask Method:

    • The removeTask method removes a task from the tasks array based on its index, prints a message indicating that the task has been removed, and sends the updated array through the tasksPublisher.
  7. getTasksResultString Method:

    • The getTasksResultString method returns the string array containing all the current tasks.

This TaskListViewModel is designed for managing a collection of tasks, providing methods for adding, removing, and retrieving task information. The use of a publisher (tasksPublisher) allows for reactive programming, notifying subscribers of changes in the tasks array.

// Model for a task
struct Task: Identifiable {
 let id: Int
 let title: String
}

// ViewModel to manage tasks
class TaskListViewModel {
 static let viewModel = TaskListViewModel()
 private init() {
 }
 var tasks: [Task] = []
 let tasksPublisher = CurrentValueSubject<[Task], Never>([])
 private var cancellables: [AnyCancellable] = [] // Use non-optional array

 func addTask(_ task: Task) {
   tasks.append(task)
   print("Task added: \(task.title)")
   tasksPublisher.send(tasks)
 }

 func removeTask(_ task: Task) {
   tasks.remove(at: task.id)
   print("Task removed: \(task.title)")
   tasksPublisher.send(tasks)
 }

   func getTasksResultString() -> [String] {
    var taskStringArray = [String]()
    print("Current tasks:")
    for task in tasks {
      taskStringArray.append(task.title)
    }
    return taskStringArray
  }

 func subscribeToChanges(handler: @escaping ([Task]) -> Void) {
         tasksPublisher
             .sink { tasks in
                 handler(tasks)
             }
             .store(in: &cancellables)
 }
}

Design UI for Task Management App

The activity’s XML layout defines the user interface for the task management app on Android. Key UI widgets to be added in XML include:

  1. RecyclerView for Task List:

    • The RecyclerView with the ID recyclerview is the focal point for displaying the list of tasks. It's configured to match the parent width and have a dynamic height based on its content.
  2. Floating Action Button (FAB) for Adding Tasks:

    • The FloatingActionButton with the ID fab serves as a prominent button for adding new tasks. It is positioned at the bottom right of the screen and adorned with a plus icon.
  3. Top Layout for App Logo and Title:

    • The topLayout contains an ImageView displaying the app logo (scade_logo) and a TextView presenting the title "Task Management App." This layout is oriented vertically and centered on the screen.
  4. Layout for No Tasks Present:

    • The noTasksPresentLayout is a container that holds a TextView with a message ("No Tasks present. Add a Task!"). It is initially set to gone and becomes visible when there are no tasks to display.
<?xml version="1.0" encoding="utf-8"?>
<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:id="@+id/topLayout"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="vertical">

       <ImageView
           android:layout_width="95dp"
           android:layout_height="wrap_content"
           android:layout_gravity="center"
           android:layout_margin="4dp"
           android:adjustViewBounds="true"
           android:src="@drawable/scade_logo" />

       <TextView
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_gravity="center"
           android:layout_marginTop="8dp"
           android:gravity="center"
           android:padding="8dp"
           android:text="Task Management App"
           android:textColor="@color/black"
           android:textSize="20sp"
           android:textStyle="bold" />

   </LinearLayout>

   <LinearLayout
       android:id="@+id/layoutTasks"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_below="@+id/topLayout"
       android:layout_margin="8dp"
       android:orientation="vertical">

       <androidx.recyclerview.widget.RecyclerView
           android:id="@+id/recyclerview"
           android:layout_width="match_parent"
           android:layout_height="wrap_content" />

   </LinearLayout>

   <com.google.android.material.floatingactionbutton.FloatingActionButton
       android:id="@+id/fab"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_alignParentRight="true"
       android:layout_alignParentBottom="true"
       android:layout_gravity="bottom|end"
       android:layout_margin="16dp"
       app:srcCompat="@android:drawable/ic_input_add" />

   <LinearLayout
       android:id="@+id/noTasksPresentLayout"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:layout_centerHorizontal="true"
       android:layout_centerVertical="true"
       android:orientation="vertical"
       android:visibility="gone">

       <TextView
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_gravity="center"
           android:gravity="center"
           android:padding="8dp"
           android:text="No Tasks present. \nAdd a Task!"
           android:textColor="@color/red"
           android:textSize="18sp" />
   </LinearLayout>
</RelativeLayout>

The structure emphasizes a clean and intuitive design, where the RecyclerView is central for visualizing tasks, and user prompts are presented elegantly for adding tasks when the list is empty. Also, We will dynamically add tasks at runtime, and an EditText is employed within an AlertDialog for user input at the click of fab button.

Create RecyclerView Adapter to Display Tasks

The Android RecyclerViewAdapter efficiently connects a list of task models to a RecyclerView. It utilizes a MyViewHolder to define the appearance of each task, with a TextView displaying the task title.

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.MyViewHolder> {
   private Context context;
   private List<String> taskModels;
   private final OnTaskItemClickListner taskItemClickListner;

   public RecyclerViewAdapter(Context context, List<String> taskModels, OnTaskItemClickListner taskItemClickListner) {
       this.context = context;
       this.taskModels = taskModels;
       this.taskItemClickListner = taskItemClickListner;
   }

   public interface OnTaskItemClickListner {
       void onTaskItemClick(String taskModel);

       boolean onTaskItemLongClick(String taskModel);
   }

   @NonNull
   @Override
   public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
       View itemView = LayoutInflater.from(context).inflate(R.layout.task_item_layout, parent, false);
       return new MyViewHolder(itemView);
   }

   @Override
   public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
       holder.bind(taskModels.get(position), taskItemClickListner);
   }

   @Override
   public int getItemCount() {
       return taskModels.size();
   }

   class MyViewHolder extends RecyclerView.ViewHolder {
       TextView titleTV;

       public MyViewHolder(@NonNull View itemView) {
           super(itemView);
           titleTV = (TextView) itemView.findViewById(R.id.titleTV);
       }

       public void bind(final String taskModel, final OnTaskItemClickListner taskItemClickListner) {
           titleTV.setText(taskModel);
           itemView.setOnClickListener(new View.OnClickListener() {
               @Override
               public void onClick(View view) {
                   taskItemClickListner.onTaskItemClick(taskModel);
               }
           });

           itemView.setOnLongClickListener(new View.OnLongClickListener() {
               @Override
               public boolean onLongClick(View view) {
                   return taskItemClickListner.onTaskItemLongClick(taskModel);
               }
           });
       }
   }
}

The adapter's interface, OnTaskItemClickListner, facilitates item click and long-click events. Clicking an item triggers the onTaskItemClick method, while a long-click invokes onTaskItemLongClick. This streamlined adapter ensures the separation of concerns, managing UI-related tasks, and delegating interaction events to the implementing class.

Define Swift Methods in Android’s activity

We will first initialize the Swift runtime in activity by calling SwiftFoundation.Initialize. The first argument is a pointer to the Java context (activity), and the second argument is always set to false. Then it loads the dynamic library "SwiftAndroidExample" containing Swift code using System.loadLibrary.

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");

Initialize the TaskManager class

The Android declaration introduces the native method initTaskManager(), acting as a bridge between Java and Swift in the Android app. \

private native void initTaskManager();

Its Swift implementation, denoted by MainActivity_initTaskManager, leverages the @_silgen_name attribute for native function linkage. This Swift method, residing in the MainActivity, initializes the Task Manager by creating a JObject wrapper for the Android activity object. It then prepares the associated Swift TaskListViewModel by calling the updateTasksList method, ensuring seamless integration and readiness for adding or deleting tasks within the Android app.

// NOTE: Use @_silgen_name attribute to set native name for a function called from Java
@_silgen_name("Java_com_example_swiftandroidexample_MainActivity_initTaskManager")
public func MainActivity_initTaskManager(
 env: UnsafeMutablePointer<JNIEnv>, activity: JavaObject
) {
  // Create JObject wrapper for activity object
  let mainActivity = JObject(activity)
  let viewModel = TaskListViewModel.viewModel

  viewModel.subscribeToChanges { _ in
    updateTasksList(activity: mainActivity)
  }

  updateTasksList(activity: mainActivity)
}

Native Methods to add/delete a Task

The Android native methods addTask and removeTask, declared in the Java activity, find their Swift implementations through the @_silgen_name attribute, ensuring seamless cross-language integration.

public native void addTask(String taskName);

public native void removeTask(String taskName);

The Swift MainActivity_addTask method receives a task name from Java, converts it to a Swift string, creates a new task, and adds it to the TaskListViewModel. Similarly, MainActivity_removeTask removes the specified task from the viewModel's task list. Both Swift methods conclude by calling updateTasksList to synchronize the changes back to the Android activity, demonstrating a bi-directional flow of task management between Java and Swift in the Android app.

// NOTE: Use @_silgen_name attribute to set native name for a function called from Java
@_silgen_name("Java_com_example_swiftandroidexample_MainActivity_addTask")
public func MainActivity_addTask(
 env: UnsafeMutablePointer<JNIEnv>, activity: JavaObject,
 taskName: JavaString
) {
 // Create JObject wrapper for activity object
 let mainActivity = JObject(activity)

 // Convert the Java string to a Swift string
 let taskTitle = String.fromJavaObject(taskName)

 let viewModel = TaskListViewModel.viewModel

 let task1 = Task(id: viewModel.tasks.count, title: taskTitle)

 viewModel.addTask(task1)
}
// NOTE: Use @_silgen_name attribute to set native name for a function called from Java
@_silgen_name("Java_com_example_swiftandroidexample_MainActivity_removeTask")
public func MainActivity_removeTask(
 env: UnsafeMutablePointer<JNIEnv>, activity: JavaObject,
 taskName: JavaString
) {
 // Create JObject wrapper for activity object
 let mainActivity = JObject(activity)

 // Convert the Java string to a Swift string
 let taskTitle = String.fromJavaObject(taskName)

 let viewModel = TaskListViewModel.viewModel

 for task in viewModel.tasks {
   if task.title == taskTitle {
     viewModel.removeTask(task)
   }
 }
}

Display current Tasks in RecyclerView

In the Swift code, the updateTasksList method orchestrates the synchronization of task data between Swift and Java in the Android app. By accessing the TaskListViewModel, it retrieves the result string representing the current tasks. This string is then passed to the Java activity through the activity.call method, invoking the printTasks method in Java.

public func updateTasksList(activity: JObject) {
  let viewModel = TaskListViewModel.viewModel
  let tasksList = viewModel.getTasksResultString()
  activity.call(method: "printTasks", tasksList)
}

The corresponding printTasks method in Java receives the task result string. If the string is empty, it adjusts the visibility of UI elements to prompt the user to add tasks. Otherwise, it parses the tasks from the string, configures a RecyclerViewAdapter with a click listener for removing tasks, and dynamically updates the RecyclerView to display the tasks in the Android activity. This seamless interaction exemplifies the bidirectional communication between Swift and Java for effective task management in cross-platform applications.

    public void printTasks(String[] taskResultString) {
        if (taskResultString.length == 0) {
            // show add tasks
            findViewById(R.id.noTasksPresentLayout).setVisibility(View.VISIBLE);
            findViewById(R.id.layoutTasks).setVisibility(View.GONE);
            return;
        }
        findViewById(R.id.noTasksPresentLayout).setVisibility(View.GONE);
        findViewById(R.id.layoutTasks).setVisibility(View.VISIBLE);
        List<String> tasks = getTasksFromString(taskResultString);
        RecyclerViewAdapter recyclerViewAdapter = new RecyclerViewAdapter(MainActivity.this, tasks, new RecyclerViewAdapter.OnTaskItemClickListner() {
            @Override
            public void onTaskItemClick(String taskModel) {
                showDialogToRemoveTask(taskModel);
            }

            @Override
            public boolean onTaskItemLongClick(String taskModel) {
                return false;
            }
        });
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(MainActivity.this);
        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerView.setAdapter(recyclerViewAdapter);
    }

Implement Dialog to Remove Tasks

The showDialogToRemoveTask method in Android presents an AlertDialog to confirm the removal of a task. The dialog displays the task name in the title, providing clarity to the user. The positive button, labeled "Delete," triggers the removeTask method if clicked, initiating the removal process. On the negative button, labeled "Cancel," the dialog is dismissed, offering users the option to reconsider. This interactive and user-friendly dialog, coupled with Swift's implementation of the removeTask method, ensures a seamless and intuitive task removal experience in the cross-platform application.

private void showDialogToRemoveTask(String taskModel) {
   AlertDialog.Builder builder = new AlertDialog.Builder(this);
   builder.setTitle("Remove the task: " + taskModel);

   // Set up the buttons
   builder.setPositiveButton("Delete", new DialogInterface.OnClickListener() {
       @Override
       public void onClick(DialogInterface dialog, int which) {
           removeTask(taskModel);
       }
   });
   builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
       @Override
       public void onClick(DialogInterface dialog, int which) {
           dialog.cancel();
       }
   });

   builder.show();

}

Run & Test the App!

Let's test the app now & check if OpenCombine smoothly updates the task list in the RecyclerView as we add or remove the tasks. Keep an eye on the user interface, and ensure the tasks reflect real-time changes, validating the effectiveness of OpenCombine in seamless cross-platform communication.

Add Task:

Image description

Remove Task:

Image description

Trying out OpenCombine in Android Studio for our task management app was pretty cool! It brings the power of reactive Swift programming to Android, making cross-platform development a breeze. Give it a shot and if you run into any hiccups, hop into our SCADE’s Discord community – we're always here to help! 🚀