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:
Display list of GitHub followers using Github API & Swift PM library - https://medium.com/@SCADE/implement-recyclerview-using-swift-pm-libraries-eacc1efd48af
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.
https://drive.google.com/file/d/1Eq12boTsBkUpg8VYQPUo79pyDbReNIiq/view?usp=sharing
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:
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.
Operators: Combine provides a set of operators that allow developers to transform, filter, and combine streams of values.
Error Handling: Combine includes mechanisms for handling errors in a reactive way, making it well-suited for handling asynchronous tasks.
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.
- Task Struct:
- There's a
Task
struct representing a model for a task with two properties:id
of typeInt
andtitle
of typeString
. The struct conforms to theIdentifiable
protocol.
- 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.
- Tasks Array:
- The class contains a
tasks
property, which is an array ofTask
objects. This array serves as the storage for the tasks managed by the view model.
- Tasks Publisher:
- There's a
tasksPublisher
property, which is an instance ofPassthroughSubject<[Task], Never>
. This subject acts as a publisher for changes in the tasks array.
- addTask Method:
- The
addTask
method adds a new task to thetasks
array, prints a message indicating that the task has been added, and then sends the updated array through thetasksPublisher
.
- removeTask Method:
- The
removeTask
method removes a task from thetasks
array based on its index, prints a message indicating that the task has been removed, and sends the updated array through thetasksPublisher
.
- 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:
- RecyclerView for Task List:
- The
RecyclerView
with the IDrecyclerview
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.
- Floating Action Button (FAB) for Adding Tasks:
- The
FloatingActionButton
with the IDfab
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.
- Top Layout for App Logo and Title:
- The
topLayout
contains anImageView
displaying the app logo (scade_logo
) and aTextView
presenting the title "Task Management App." This layout is oriented vertically and centered on the screen.
- Layout for No Tasks Present:
- The
noTasksPresentLayout
is a container that holds aTextView
with a message ("No Tasks present. Add a Task!"). It is initially set togone
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:
Remove Task:
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! 🚀
Top comments (0)