Skip to main content

137 posts tagged with "Apps"

View All Tags

USING METHOD CHANNELS TO ENABLE CALLS BETWEEN NATIVE CODE AND FLUTTER CODE

Published: · Last updated: · 5 min read
Appxiom Team
Mobile App Performance Experts

Flutter, a popular cross-platform development framework, allows developers to build high-performance applications with a single codebase. However, there are times when you need to integrate platform-specific functionality into your Flutter app. Method Channels provide a powerful mechanism to bridge the gap between Flutter and native code, enabling you to call native methods from Flutter and vice versa.

In this blog, we'll explore how to utilize Method Channels to invoke native code in both Android and iOS platforms from your Flutter app.

Prerequisites

To follow along with this tutorial, you should have a basic understanding of Flutter and have Flutter SDK installed on your machine.

Additionally, make sure you have the necessary tools and configurations set up for Android and iOS development, such as Android Studio and Xcode.

Implementing Method Channels in Flutter

Step 1: Create a Flutter Project Let's start by creating a new Flutter project. Open your terminal or command prompt and run the following command:

flutter create method_channel_demo
cd method_channel_demo

Step 2: Add Dependencies Open the pubspec.yaml file in your project's root directory and add the following dependencies:

dependencies:flutter:sdk: flutter
dev_dependencies:flutter_test:sdk: flutter

Save the file and run flutter pub get in your terminal to fetch the dependencies.

Step 3: Define the Native Method Channel Create a new Dart file named method_channel.dart in the lib directory. In this file, define a class called MethodChannelDemo that will encapsulate the native method channel communication. Add the following code:

import 'package:flutter/services.dart';

class MethodChannelDemo {
static const platform = MethodChannel('method_channel_demo');

static Future<String> getPlatformVersion() async {
return await platform.invokeMethod('getPlatformVersion');
}
}

In this code, we define a static platform object of type MethodChannel and associate it with the channel name 'method_channel_demo'. We also define a getPlatformVersion() method that invokes the native method 'getPlatformVersion' using the invokeMethod() function.

Step 4: Implement Native Code Next, let's implement the native code for both Android and iOS platforms.

For Android, open the MainActivity.kt file and import the necessary packages:

import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugin.common.MethodChannel

Inside the MainActivity class, override the configureFlutterEngine() method and register the method channel:

class MainActivity : FlutterActivity() {
private val CHANNEL = "method_channel_demo"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)

MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
if (call.method == "getPlatformVersion") {
result.success("Android ${VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
}
}

The code above sets up a method channel with the same name as defined in the Dart code. It handles the method call with a lambda function where we check the method name and return the Android platform version using the result.success() method.

For iOS, open the AppDelegate.swift file and import the necessary packages:

import UIKit
import Flutter
import UIKit.UIApplication
import UIKit.UIWindow

Inside the AppDelegate class, add the following code to register the method channel:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private let CHANNEL = "method_channel_demo"
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

GeneratedPluginRegistrant.register(with: self)
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: CHANNEL,
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "getPlatformVersion" {
result("iOS " + UIDevice.current.systemVersion)
} else {
result(FlutterMethodNotImplemented)
}
})

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

In this code, we create a method channel with the same name as defined in the Dart code. We handle the method call using a closure, check the method name, and return the iOS platform version using the result() method.

Step 5: Call Native Code from Flutter Now that we have set up the method channels and implemented the native code, let's invoke the native methods from Flutter.

Open the lib/main.dart file and replace its contents with the following code:

import 'package:flutter/material.dart';
import 'method_channel.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Method Channel Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FutureBuilder<String>(
future: MethodChannelDemo.getPlatformVersion(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Platform version: ${snapshot.data}');
} else if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return CircularProgressIndicator();
},
),
],
),
),
),
);
}
}

In this code, we import the method_channel.dart file and create a simple Flutter app with a centered column containing a FutureBuilder. The FutureBuilder calls the getPlatformVersion() method and displays the platform version once it's available.

Step 6: Run the App Finally, we're ready to run our app. Connect a physical device or start an emulator, then run the following command in your terminal:

flutter run

You have successfully implemented Method Channels to call native code in Android and iOS platforms from your Flutter app. You can now leverage this mechanism to access platform-specific APIs and extend the functionality of your Flutter applications.

Conclusion

In this tutorial, we explored how to utilize Method Channels to invoke native code in Android and iOS platforms from a Flutter app. We covered the steps required to set up the method channels, implemented the native code for Android and iOS, and demonstrated how to call native methods from Flutter. By leveraging Method Channels, Flutter developers can access platform-specific features and create powerful cross-platform applications. Happy coding!

BUILDING ANDROID APPS WITH KOTLIN AND ROOM

Published: · Last updated: · 4 min read
Appxiom Team
Mobile App Performance Experts

In today's world of mobile app development, efficient data management is crucial for creating high-quality applications. Kotlin, a modern programming language, offers great support for Android development. When combined with Room, an SQLite object-relational mapping (ORM) library, developers can streamline database operations and enhance productivity.

In this blog post, we will explore the fundamentals of working with Kotlin and Room and demonstrate how to leverage their features to build robust and efficient Android applications.

Prerequisites

To follow along with the examples in this blog post, you should have a basic understanding of Kotlin and Android development. Familiarity with SQLite databases would also be helpful. Ensure you have Android Studio installed and set up on your machine.

What is Room?

Room is an Android library that provides an abstraction layer over SQLite, allowing developers to work with databases using Kotlin or Java objects. It simplifies the process of defining and interacting with databases by eliminating boilerplate code and providing compile-time checks for SQL statements.

Room consists of three main components: entities, data access objects (DAOs), and the database itself.

Setting Up Room in Android Project

To begin, create a new Android project in Android Studio or open an existing one. Then, follow these steps to add the necessary dependencies for Room:

  1. Open the app-level build.gradle file.

  2. Add the following dependencies in the dependencies block:

implementation 'androidx.room:room-runtime:x.y.z'
kapt 'androidx.room:room-compiler:x.y.z'

Replace x.y.z with the latest version of Room available. Make sure to check the official documentation or Maven repository for the most up-to-date version.

  1. Sync your project to fetch the new dependencies.

Defining Entities

Entities represent the tables in the database. Each entity class represents a table, and its fields represent the columns. Let's create a simple entity called User:

@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
val email: String
)

Here, we annotate the class with @Entity and specify the table name as "users." The @PrimaryKey annotation marks the id field as the primary key.

Creating a Data Access Object (DAO)

A DAO provides methods to perform CRUD (Create, Read, Update, Delete) operations on the database. Let's create a DAO interface for the User entity:

@Dao
interface UserDao {
@Insert
fun insert(user: User)

@Query("SELECT * FROM users")
fun getAllUsers(): List<User>

@Query("SELECT * FROM users WHERE id = :userId")
fun getUserById(userId: Int): User?

@Update
fun updateUser(user: User)

@Delete
fun deleteUser(user: User)
}

In this example, we annotate the interface with @Dao to mark it as a DAO. We define several methods annotated with @Insert, @Query, @Update, and @Delete for different database operations.

Creating the Database

Now, let's create the database class that ties everything together:

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao

companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
}

private fun buildDatabase(context: Context) =
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
}
}

Here, we annotate the class with @Database and specify the entities it contains (in this case, only User) and the database version. The AppDatabase class is an abstract class that extends RoomDatabase. We define an abstract method userDao() that returns the DAO interface for the User entity.

We also implement the Singleton pattern to ensure that only one instance of the database is created. The getInstance() method returns the singleton instance of the AppDatabase. If the instance is null, it creates a new instance using the buildDatabase() method.

Performing Database Operations: Now that we have set up the entities, DAO, and database, let's explore how to perform database operations:

val user = User(1, "John Doe", "john.doe@example.com")
val userDao = AppDatabase.getInstance(context).userDao()

// Inserting a user
userDao.insert(user)

// Fetching all users
val allUsers = userDao.getAllUsers()

// Fetching a user by ID
val retrievedUser = userDao.getUserById(1)

// Updating a user
user.name = "Jane Doe"
userDao.updateUser(user)

// Deleting a user
userDao.deleteUser(user)

In the above example, we first create a User object and obtain an instance of the UserDao using the getInstance() method of the AppDatabase class. We can then perform various operations, such as inserting, fetching, updating, and deleting users.

Conclusion

Kotlin and Room provide a powerful combination for working with databases in Android applications. With Room's simplified API and compile-time checks, developers can write efficient and maintainable code.

In this blog post, we covered the basics of working with Kotlin and Room, including setting up dependencies, defining entities, creating DAOs, and performing common database operations. By leveraging these tools, you can streamline your Android app's data management and create robust applications with ease.

Remember to refer to the official documentation for Room and Kotlin for more in-depth information and advanced features.

Happy coding!

EXPLORING APPLE WWDC 2023: MAJOR FEATURE ANNOUNCEMENTS FOR IOS DEVELOPERS

Published: · Last updated: · 8 min read
Don Peter
Cofounder and CTO, Appxiom

Apple's Worldwide Developers Conference (WWDC) is an eagerly anticipated annual event where the company unveils its latest software updates and development tools. In 2023, WWDC introduced several exciting features for developers, aimed at enhancing the app development experience and expanding the reach of apps across various Apple devices.

Let's dive into the major feature releases for developers announced at Apple WWDC 2023.

Swift Macro

Version 5.9 introduced the concept of macros to Swift. Macros can be categorized into multiple smaller types.

  • ExpressionMacro to generate expression.

  • AccessorMacro to add getters and setters.

  • ConformanceMacro makes a type conform to a protocol.

Let's take a look at a basic macro to see how they function. Macros have the advantage of being executed during compile time.

Defining the AuthorMacro

One useful macro can be created to generate the file author name.

In MyMacrosPlugin.swift:

import Foundation
import SwiftSyntax
import SwiftSyntaxMacros

public struct AuthorMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
let argument = node.argumentList.first?.expression
let segments = argument.as(StringLiteralExprSyntax.self)?.segments

return "Autor: \(segments.first.content.text)"
}
}

This code defines a Swift macro named AuthorMacro that prints the author name from the string literal passed to it.

  • The AuthorMacro struct implements the ExpressionMacro protocol, allowing it to expand macros involving expressions.

  • The expansion function takes in a macro invocation and context and performs the following checks:

It ensures that the macro is invoked with a single argument that is a static string literal.

  • It appends the greeting message to the string.

The function returns an expression representing the constructed greeting message.

Declare Macro in Main Project

@freestanding(expression) 
public macro author(_ stringLiteral: String) -> String =
#externalMacro(module: "MyMacrosPlugin", type: "AuthorMacro")

Adding a string parameter and declaring the macro in our app target is a straightforward process. By incorporating the string parameter, we can enhance the macro's functionality and customize its behavior based on the specific needs of our application.

This flexibility allows us to pass dynamic string values to the macro, enabling more versatile and adaptable macro expansions.

Calling the Macro

print(#author("Mark")) //prints "Author: Mark"

In order to use this macro simply call #author and pass the String as parameter. The macro will print the Author name.

Macros can be a powerful tool for improving the readability, performance, and functionality of your Swift code. However, it is important to use them carefully, as they can also make your code more difficult to understand and maintain.

Here are some tips for using macros:

  • Keep your macros short and simple.

  • Use descriptive names for your macros.

  • Document your macros thoroughly.

  • Test your macros thoroughly.

  • Use macros sparingly.

By following these tips, you can use macros to write more concise, efficient, and powerful Swift code.

SwiftData

One of the highlights of Apple WWDC 2023 was the introduction of SwiftData. This new framework enables developers to seamlessly connect their data models to the user interface in SwiftUI.

Creating a Model

To enable saving instances of a model class using SwiftData, import the framework and annotate the class with the Model macro. This macro modifies the class to conform to the PersistentModel protocol, which SwiftData utilizes to analyze the class and generate an internal schema.

By default, SwiftData includes all noncomputed properties of a class, provided they use compatible types. The framework supports primitive types like Bool, Int, and String, as well as more complex value types such as structures, enumerations, and other types that conform to the Codable protocol.

import SwiftData

// Annotate with the @Model macro.
@Model
class Task {
var name: String
var role: String
var startDate: Date
var endDate: Date
var owner: Owner?
}

Leveraging Swift's macro system, developers can enjoy a streamlined API for modeling data using the familiar Codable protocol.

Persisting a Model

To persist a model instance by SwiftData, insert the instance into the context using the insert function.

var task = Task(name: name, 
role: role,
startDate: startDate,
endDate: endDate)

context.insert(task)

After performing the insert, you have two options for saving the changes. The first option is to explicitly call the save() method on the context immediately. This will persist the changes to the underlying data store.

Alternatively, you can rely on the context's implicit save behavior. Contexts automatically track changes made to their known model instances, and these changes will be included in subsequent saves without requiring explicit invocation of the save() method. The context will take care of persisting the changes to the data store as needed.

Fetching a Model

To fetch instances of a model and optionally apply search criteria and a preferred sort order in your SwiftUI view, you can use the @Query property wrapper. Additionally, by using the @Model macro, you can add Observable conformance to your model classes.

This enables SwiftUI to automatically refresh the containing view whenever changes occur to any of the fetched instances.

import SwiftUI
import SwiftData

struct ContentView: View {
@Query(sort: \.endDate, order: .reverse) var allTasks: [Task]

var body: some View {
List {
ForEach(allTasks) { task in
TaskView(for: task)
}
}
}
}

WidgetKit

This major feature release empowers developers to extend their app's content beyond the app itself. With WidgetKit, developers can create glanceable, up-to-date experiences in the form of widgets, Live Activities, and watch complications.

@main
struct WeatherStatusWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(
kind: "",
provider: WeatherStatusProvider()
) { entry in
WeatherStatusView(entry.weatherStatus)
}
.configurationDisplayName("Weather Status")
.description("Shows an overview of your weather status")
.supportedFamilies([.systemSmall])
}
}

The technology and design similarities among widgets, Live Activities, and watch complications facilitate seamless feature development and usage across different contexts.

ActivityKit

ActivityKit offers developers the ability to create Live Activities that provide live updates and interactions directly from their apps. Live Activities can appear in prominent positions such as the Lock Screen, Dynamic Island, and as banners on the Home Screen. Users can view real-time information, launch the app, and perform specific functionalities through buttons and toggles, without fully opening the app.

import SwiftUI
import WidgetKit

@main
struct FoodOrderActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: FoodOrderAttributes.self) { context in

} dynamicIsland: { context in

}
}
}

By leveraging SwiftUI and WidgetKit, developers can share code between widgets and Live Activities, making it easier to build engaging experiences.

Observable

The Observable protocol simplifies the implementation of data change notifications. By attaching the Observable macro to custom types, developers indicate conformance to the Observable protocol. This protocol enables types to emit notifications to observers whenever the underlying data changes.

@Observable final class Animal {
var name: String = ""
var sleeping: Bool = false

init(name: String, sleeping: Bool = false) {
self.name = name
self.sleeping = sleeping
}
}

To enable change tracking, use the withObservationTracking(_:onChange:) function. In the provided code example, this function is used to call the onChange closure when the name property of a car changes. However, it does not trigger the closure when the sleeping flag of the car changes. This behavior occurs because the function only tracks properties that are read within its apply closure, and in this case, the sleeping property is not read within that closure.

func render() {
withObservationTracking {
for animal in animals { //apply closure
print(animal.name)
}
} onChange: { //onChange closure
print("Call UI updation.")
}
}

The Observable protocol provides a convenient way to handle data updates and build reactive interfaces, enhancing the overall user experience of the app.

WorkoutKit

This powerful framework offers models and utilities for creating and previewing workout compositions in iOS and watchOS apps. Developers can design various types of workouts, including CustomWorkoutComposition, GoalWorkoutComposition, and others catering to different fitness activities. The framework provides methods for validating, exporting, and previewing workouts, allowing users to save compositions to the Workout app. Furthermore,

WorkoutKit enables developers to create and manage workout schedules, sync scheduled compositions to Apple Watch, and query completed workouts.

PayLaterView

Showcasing Apple Pay Later Feature Apple Pay Later, a new financial service, received special attention at WWDC 2023. To enhance its visibility, Apple introduced the PayLaterView, a dedicated view for displaying the Apple Pay Later visual merchandising widget.

VisionOS

One of the key features of VisionOS is the ability to create multiple windows within the app. These windows, built using SwiftUI, provide familiar views and controls while enabling developers to add depth by incorporating stunning 3D content. With VisionOS, it is possible to further enhance the app's depth by incorporating 3D volumes.

These volumes, powered by RealityKit or Unity, allows to showcase captivating 3D content that can be viewed from any angle within the Shared Space or an app's Full Space. The flexibility of volumes helps to craft engaging experiences that captivate and delight app users.

By default, apps in VisionOS launch into the Shared Space, where they coexist side-by-side, akin to multiple apps on a Mac desktop. Utilizing windows and volumes, apps can display their content within this shared environment, giving users the ability to freely reposition and interact with these elements. For a truly immersive experience, apps can open a dedicated Full Space, where only their content is visible. Within a Full Space, apps can leverage windows and volumes, create unbounded 3D content, open portals to different worlds, or provide users with a fully immersive environment.

Conclusion

Apple WWDC 2023 brought significant enhancements for developers, offering tools and frameworks to streamline data modeling, extend app content through widgets and Live Activities, simplify data change notifications, optimize workout compositions, and showcase new financial features.

These advancements empower developers to create more immersive and feature-rich applications across Apple's ecosystem of devices.

DATA PERSISTENCE IN FLUTTER

Published: · Last updated: · 5 min read
Appxiom Team
Mobile App Performance Experts

In today's app development landscape, databases play a crucial role in managing and storing data. Flutter, a popular cross-platform framework, offers various options for integrating databases into your applications.

In this blog, we will explore the fundamental database concepts in Flutter and provide code examples to illustrate their implementation. So, let's dive in and learn how to effectively work with databases in Flutter!

Introduction to Databases

A database is a structured collection of data that allows efficient storage, retrieval, and manipulation of information. In the context of app development, databases are used to store and manage data persistently, enabling apps to function seamlessly even when offline or across different devices.

Local Data Persistence in Flutter

Local data persistence refers to the storage of data on the device itself. Flutter provides several libraries and techniques for local data persistence.

Some popular options include:

Shared Preferences

Shared Preferences is a simple key-value store that allows you to store primitive data types such as strings, integers, booleans, etc. It's suitable for storing small amounts of data that don't require complex querying.

import 'package:shared_preferences/shared_preferences.dart';

void saveData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('username', 'JohnDoe');
}

void loadData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String username = prefs.getString('username');
print('Username: $username');
}

Hive

Hive is a lightweight and fast NoSQL database for Flutter. It offers a simple key-value store as well as support for more complex data structures. Hive is known for its excellent performance and ease of use.

import 'package:hive/hive.dart';

void saveData() async {
var box = await Hive.openBox('myBox');
await box.put('username', 'JohnDoe');
}

void loadData() async {
var box = await Hive.openBox('myBox');
String username = box.get('username');
print('Username: $username');
}

SQLite Database Integration

SQLite is a widely used relational database management system (RDBMS) that provides a self-contained, serverless, and zero-configuration SQL database engine. Flutter offers seamless integration with SQLite, enabling you to create and manage structured databases efficiently.

Setting up SQLite in Flutter

To use SQLite in Flutter, you need to include the sqflite package in your pubspec.yaml file and import the necessary dependencies.

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

Future<Database> initializeDatabase() async {
String path = join(await getDatabasesPath(), 'my_database.db');
return await openDatabase(
path,
version: 1,
onCreate: (Database db, int version) async {
// Create tables and define schemas
await db.execute(
'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)',
);
},
);
}

Performing CRUD Operations with SQLite

Once the database is initialized, you can perform various CRUD (Create, Read, Update, Delete) operations on it using SQL queries.

Future<void> insertUser(User user) async {
final db = await database;
await db.insert(
'users',
user.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}

Future<List<User>> getUsers() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query('users');
return List.generate(maps.length, (i) {
return User(
id: maps[i]['id'],
name: maps[i]['name'],
);
});
}

Working with Firebase Realtime Database

Firebase Realtime Database is a NoSQL cloud-hosted database that enables real-time data synchronization across devices. It offers seamless integration with Flutter, allowing you to store and sync structured data easily.

Setting up Firebase Realtime Database

To use Firebase Realtime Database in Flutter, you need to create a Firebase project, add the necessary dependencies in your pubspec.yaml file, and configure Firebase in your Flutter app.

Performing CRUD Operations with Firebase Realtime Database

Firebase Realtime Database uses a JSON-like structure to store and organize data. You can perform CRUD operations using the Firebase SDK.

import 'package:firebase_database/firebase_database.dart';

void insertUser(User user) {
DatabaseReference usersRef =
FirebaseDatabase.instance.reference().child('users');
usersRef.push().set(user.toJson());
}

void getUsers() {
DatabaseReference usersRef =
FirebaseDatabase.instance.reference().child('users');
usersRef.once().then((DataSnapshot snapshot) {
Map<dynamic, dynamic> values = snapshot.value;
values.forEach((key, values) {
print('ID: $key');
print('Name: ${values['name']}');
});
});
}

Implementing GraphQL with Hasura and Flutter

GraphQL is a query language for APIs that provides a flexible and efficient approach to data fetching. Hasura is an open-source engine that provides instant GraphQL APIs over databases. By combining Flutter, Hasura, and GraphQL, you can create powerful and responsive apps with real-time data capabilities.

Setting up Hasura and GraphQL in Flutter

To integrate Hasura and GraphQL into your Flutter app, you need to set up a Hasura server and define your database schema. Then, use the graphql package in Flutter to interact with the GraphQL API.

Performing GraphQL Operations with Hasura and Flutter

With GraphQL, you can define queries and mutations to fetch and modify data from the server.

import 'package:graphql_flutter/graphql_flutter.dart';

void getUsers() async {
final String getUsersQuery = '''
query {
users {
id
name
}
}
''';

final GraphQLClient client = GraphQLClient(
cache: GraphQLCache(),
link: HttpLink('https://your-hasura-endpoint.com/v1/graphql'),
);

final QueryResult result = await client.query(QueryOptions(
document: gql(getUsersQuery),
));

if (result.hasException) {
print(result.exception.toString());
} else {
final List<dynamic> users = result.data['users'];
for (var user in users) {
print('ID: ${user['id']}');
print('Name: ${user['name']}');
}
}
}

Conclusion

In this blog, we explored various database concepts in Flutter and learned how to implement them using different database technologies. We covered local data persistence, SQLite integration, Firebase Realtime Database, and GraphQL with Hasura.

With these skills, you can efficiently manage and store data in your Flutter applications. Experiment with these concepts and choose the most suitable database solution based on your app's requirements.

Happy coding!

Remember to import the necessary packages and dependencies to execute the code examples provided in this blog.

A GUIDE TO WRITE KOTLIN ANDROID APPS WITH MINIMAL BATTERY USAGE

Published: · Last updated: · 4 min read
Don Peter
Cofounder and CTO, Appxiom

In today's world, where smartphones have become an integral part of our lives, battery life is a crucial aspect for users. Android developers must strive to create applications that not only offer a rich user experience but also consume minimal battery power.

In this blog post, we will explore essential techniques and best practices for writing Android apps with optimized battery usage, enhancing user satisfaction and app performance.

1. Efficient Background Processing in Kotlin

Background processing is often a significant contributor to battery drain. It's essential to utilize Android's threading mechanisms, such as Coroutines or Executor, to offload resource-intensive tasks to separate threads. This prevents blocking the main UI thread and allows the system to enter low-power states more frequently.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

// Perform background processing using Kotlin coroutines
fun performBackgroundTask() {
GlobalScope.launch(Dispatchers.Default) {
// Perform your resource-intensive tasks here
// ...
}
}

The Dispatchers.Default dispatcher is optimized for CPU-intensive tasks and is suitable for most background processing operations.

Furthermore, consider using JobScheduler or WorkManager for scheduling periodic or network-related tasks, which allows the system to optimize their execution based on factors like network availability and device charging state.

2. Optimize Network Usage

Networking operations, such as data synchronization or retrieving updates, can consume a significant amount of battery power. Minimize network requests by batching multiple operations together and reducing unnecessary polling intervals. Employ techniques like HTTP caching, compression, and server-driven events (e.g., WebSocket or Firebase Cloud Messaging) to ensure efficient communication between the app and the server.

3. Fine-tune Location Services

Location-based apps often rely on continuous GPS updates, leading to substantial battery drain. To minimize this, use location services judiciously and leverage lower power-consuming alternatives like network-based location providers or geofencing.

Additionally, consider decreasing the frequency of location updates based on the app's requirements. Remember to release location updates when not needed and utilize the latest Android location APIs for better power efficiency.

import android.content.Context
import android.location.LocationManager

// Release location updates when they are no longer needed
fun releaseLocationUpdates(context: Context) {
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val locationListener = MyLocationListener()
// Replace with your custom location listener

locationManager.removeUpdates(locationListener)
}

Note: Ensure that you have the necessary location permissions declared in your AndroidManifest.xml file and have requested them from the user at runtime.

4. Optimize UI and Rendering

Efficient UI design and rendering play a crucial role in reducing battery consumption. Minimize the number of view updates and layout recalculations by utilizing RecyclerViews, ConstraintLayouts, and other performance-oriented UI components.

Implementing adaptive layouts that automatically adjust based on screen size and orientation can also help conserve power. Additionally, leverage tools like Systrace and Android Profiler to identify UI-related performance bottlenecks and optimize app rendering accordingly.

5. Battery-Aware Coding

Writing battery-efficient code involves considering the power implications of various operations. Avoid excessive wake locks, which prevent the device from entering low-power states. Release resources promptly, unregister receivers when not needed, and utilize the appropriate lifecycle methods to manage component activation. Use the Battery Historian tool to analyze power usage patterns and identify areas for improvement.

Lastly, encourage user involvement by providing settings or options that allow them to customize battery-consuming features.

Conclusion

As an Android developer, writing apps with minimal battery usage is a responsibility that can enhance user experience and app performance. By implementing efficient background processing, optimizing network usage, fine-tuning location services, optimizing UI and rendering, and practicing battery-aware coding, developers can create apps that consume less battery power while still delivering the desired functionality.

Prioritizing battery efficiency not only benefits users but also contributes to a sustainable and eco-friendly mobile ecosystem.

QUICK-START GUIDE FOR USING CORE DATA WITH SWIFTUI

Published: · Last updated: · 5 min read
Appxiom Team
Mobile App Performance Experts

In the world of iOS app development, Core Data is a powerful framework that enables developers to work with a persistent storage solution. With the advent of SwiftUI, Apple's modern declarative framework for building user interfaces, integrating Core Data seamlessly into SwiftUI apps has become even easier and more efficient.

In this blog post, we will explore how to use Core Data with SwiftUI, discussing the fundamental concepts and providing a step-by-step guide along with code examples.

Prerequisites

To follow along with this tutorial, you should have basic knowledge of SwiftUI and a working understanding of the Swift programming language. Additionally, make sure you have Xcode installed on your Mac.

Setting Up the SwiftUI Project

  1. Launch Xcode and create a new SwiftUI project by selecting "File" -> "New" -> "Project" and choosing the "App" template with SwiftUI selected.

  2. Provide a name for your project, select the appropriate options, and click "Next" to create the project.

  3. Once the project is created, open the ContentView.swift file and replace its contents with the following code:

import SwiftUI

struct ContentView: View {
var body: some View {
Text("Hello, Core Data!")
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Understanding Core Data

Core Data is an object graph and persistence framework provided by Apple. It allows you to manage the model layer objects in your app, including their persistence and retrieval. Core Data uses SQLite as the default persistent store, but it also supports other options.

Core Data Entities

An entity in Core Data represents a table in the underlying data model. Each entity contains attributes and relationships, which define its structure. To create an entity, follow these steps:

  1. Open the project navigator in Xcode and select the project file.

  2. Go to the "Data Model" file, typically named YourProjectName.xcdatamodeld.

  3. Click on the "+" button to add a new entity and provide a name for it (e.g., "Task").

  4. Add attributes and relationships to the entity by clicking on the "+" button in the "Attributes" and "Relationships" sections.

Creating a Core Data Model

  1. In the project navigator, select the project file.

  2. Go to the "Data Model" file.

  3. Click on the "+" button to add a new model version.

  4. Select the newly created model version, and in the "Editor" menu, choose "Add Model Configuration" to create a configuration for your model.

Working with Core Data in SwiftUI

  1. Create a new SwiftUI view for displaying your Core Data entities. For example, create a new SwiftUI file called TaskListView.swift with the following code:
import SwiftUI

struct TaskListView: View {
@Environment(\.managedObjectContext) private var viewContext

@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Task.createdAt, ascending: true)],
animation: .default)
private var tasks: FetchedResults<Task>

var body: some View {
NavigationView {
List {
ForEach(tasks) { task in
Text(task.title ?? "Untitled")
}
.onDelete(perform: deleteTasks)
}
.navigationBarItems(trailing: EditButton())
.navigationTitle("Tasks")
}
}

private func deleteTasks(offsets: IndexSet) {
withAnimation {
offsets.map { tasks[$0] }.forEach(viewContext.delete)

do {
try viewContext.save()
} catch {
let nsError = error as NSErrorfatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}

struct TaskListView_Previews: PreviewProvider {
static var previews: some View {
TaskListView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
  1. In the TaskListView, we use the @FetchRequest property wrapper to fetch the Task entities from the Core Data managed object context. We specify a sort descriptor to order the tasks by their creation date.

  2. The TaskListView contains a list of tasks fetched from Core Data. We also implement the ability to delete tasks using the onDelete modifier.

  3. To enable Core Data integration, we access the managed object context through the @Environment(.managedObjectContext) property wrapper.

  4. Finally, we add the TaskListView as the root view in the ContentView.

Persisting Data with Core Data

  1. Open the YourProjectName.xcdatamodeld file and create a new entity called "Task".

  2. Add attributes to the "Task" entity, such as "title" (String) and "createdAt" (Date).

  3. Create a new Swift file named Task+CoreDataProperties.swift and add the following code:

import Foundation
import CoreData

extension Task {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Task> {
return NSFetchRequest<Task>(entityName: "Task")
}

@NSManaged public var title: String?
@NSManaged public var createdAt: Date?
}

extension Task: Identifiable {}
  1. Build and run your app, and you should see the list of tasks fetched from Core Data. You can add, delete, and modify tasks, and the changes will be persisted automatically.

Conclusion

In this blog post, we explored how to use Core Data with SwiftUI, integrating a persistent storage solution seamlessly into our app. We learned the basics of Core Data, created entities and attributes, and built a SwiftUI view that displays and manages data from Core Data. By leveraging the power of Core Data and SwiftUI together, you can create robust and efficient iOS apps with ease.

Remember, Core Data offers many advanced features and customization options that we haven't covered in this tutorial. I encourage you to dive deeper into the Core Data framework to unleash its full potential in your SwiftUI projects.

Happy coding!

BUILDING MEMORY EFFICIENT FLUTTER APPS

Published: · Last updated: · 3 min read
Appxiom Team
Mobile App Performance Experts

In today's mobile app development landscape, memory efficiency plays a crucial role in delivering a smooth and responsive user experience. Flutter, Google's open-source UI toolkit, allows developers to create cross-platform apps with a rich set of features. However, as apps grow in complexity and data handling requirements, it becomes essential to optimize memory usage.

In this blog, we will explore some strategies and techniques to write memory efficient code in Flutter apps, ensuring optimal performance and user satisfaction.

1. Use Stateless Widgets

In Flutter, widgets are the building blocks of the UI. To conserve memory, prefer using StatelessWidget over StatefulWidget wherever possible. Stateless widgets are immutable and do not maintain any internal state. They consume less memory and are ideal for UI components that do not require frequent updates or interaction.

Example:

class MyWidget extends StatelessWidget {
final String data;

const MyWidget(this.data);

@override
Widget build(BuildContext context) {
return Text(data);
}
}

2. Dispose of Resources

When using resources like databases, network connections, or streams, it's crucial to release them properly to avoid memory leaks. Use the dispose() method provided by various Flutter classes to release resources when they are no longer needed. For example, in a StatefulWidget, override the dispose() method to clean up resources.

Example:

class MyStatefulPage extends StatefulWidget {
@override
_MyStatefulPageState createState() => _MyStatefulPageState();
}

class _MyStatefulPageState extends State<MyStatefulPage> {
DatabaseConnection _connection;

@override
void initState() {
super.initState();
_connection = DatabaseConnection();
}

@override
void dispose() {
_connection.close();
super.dispose();
}

// Rest of the widget code...
}

3. Use Efficient Data Structures

Choosing the right data structures can significantly impact memory consumption. Flutter provides various collections such as List, Set, and Map. However, be mindful of the memory requirements when dealing with large datasets. Consider using specialized collections like SplayTreeSet or LinkedHashMap that provide efficient look-up or iteration operations.

Example:

import 'dart:collection';

void main() {
var orderedSet = SplayTreeSet<String>();
orderedSet.addAll(['Apple', 'Banana', 'Orange']);

var linkedMap = LinkedHashMap<String, int>();
linkedMap['Alice'] = 25;
linkedMap['Bob'] = 30;
linkedMap['Charlie'] = 35;
}

4. Optimize Image Usage

Images often consume a significant portion of memory in mobile apps. To reduce memory usage, consider optimizing and compressing images before using them in your Flutter app. Tools like flutter_image_compress can help reduce the image size without compromising quality. Additionally, leverage techniques like lazy loading and caching to load images only when necessary.

Example:

import 'package:flutter_image_compress/flutter_image_compress.dart';

Future<void> compressImage() async {
var compressedImage = await FlutterImageCompress.compressWithFile(
'original.jpg',
quality: 85,
);

// Store or display the compressed image.
}

5. Use ListView.builder for Large Lists

When displaying large lists, prefer using ListView.builder instead of ListView to optimize memory usage. ListView.builder lazily creates and recycles widgets as they come into and go out of view. This approach avoids creating all the widgets upfront, conserving memory and improving performance.

Example:

ListView.builder(
itemCount: 1000,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
);

Conclusion

Writing memory efficient code is crucial for creating high-performance Flutter apps. By using stateless widgets, disposing of resources properly, leveraging efficient data structures, optimizing image usage, and utilizing ListView.builder, you can significantly reduce memory consumption and enhance the overall user experience. By adopting these practices, you'll be well on your way to building robust and efficient Flutter applications.

Remember, optimizing memory usage is an ongoing process, and profiling your app's memory consumption using tools like the Flutter DevTools can provide valuable insights for further improvements.

Happy coding!

OPTIMIZING NETWORK CALLS IN KOTLIN ANDROID APPS USING RETROFIT

Published: · Last updated: · 4 min read
Appxiom Team
Mobile App Performance Experts

In today's fast-paced world, mobile app performance is a critical aspect of delivering a seamless user experience. Network calls play a vital role in app functionality, but they can also introduce performance bottlenecks if not implemented efficiently.

In this blog post, we will explore strategies to implement network calls with minimal performance impact in Kotlin-based Android apps. We will cover topics such as choosing the right networking library, optimizing network calls, and implementing caching mechanisms.

1. Choosing the Right Networking Library

Selecting the appropriate networking library can significantly impact the performance of your Android app. Several popular networking libraries are available, such as Retrofit, Volley, and OkHttp. When choosing a library, consider the following factors:

1.1 Efficiency

Look for a library that is designed to handle network operations efficiently. Libraries like Retrofit and OkHttp are known for their performance optimization capabilities.

1.2 Flexibility

Ensure that the library provides flexible options to configure network requests, timeouts, headers, and other parameters.

1.3 Community Support

Check if the library has an active community and regular updates. This ensures that you will receive support and updates for any issues or improvements.

For this blog, we will use Retrofit as our networking library of choice due to its popularity, performance, and ease of use.

2. Optimizing Network Calls

Once you have chosen a networking library, there are several strategies you can employ to optimize network calls in your Android app:

2.1 Use Asynchronous Calls

Perform network operations asynchronously to prevent blocking the main UI thread. Kotlin's coroutines and Retrofit's suspend functions are a powerful combination for writing asynchronous code in a concise and readable manner.

2.2 Implement Connection Pooling

Connection pooling allows reusing established connections for subsequent requests, reducing the overhead of establishing new connections. Retrofit and OkHttp provide connection pooling out-of-the-box, which can significantly improve performance.

2.3 Enable GZIP Compression

GZIP compression reduces the size of the response payload, resulting in faster data transmission. Ensure that your server supports GZIP compression, and enable it in the networking library configuration.

2.4 Implement Pagination

When dealing with large datasets, implement pagination to fetch data in smaller chunks. This approach reduces the overall response time and improves app performance.

3. Implementing Caching Mechanisms

Implementing caching mechanisms can further enhance the performance of network calls by reducing the need for repetitive requests. Retrofit, in combination with OkHttp, offers powerful caching capabilities.

Here's a step-by-step guide to implementing caching:

Step 1: Configure the Cache

Create an instance of the Cache class in your application initialization code, specifying the cache directory and size:

val cacheSize = 10 * 1024 * 1024 // 10 MB
val cacheDirectory = File(context.cacheDir, "http-cache")
val cache = Cache(cacheDirectory, cacheSize)

Step 2: Configure the OkHttpClient

Create an instance of the OkHttpClient class and attach the cache:

val okHttpClient = OkHttpClient.Builder()
.cache(cache)
.build()

Step 3: Configure Retrofit

Use the okHttpClient instance when building the Retrofit object:

val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.build()

Step 4: Enable Caching in Retrofit Requests

In your Retrofit service interface, specify the caching behavior for each request using the @Headers annotation:

interface ApiService {
@Headers("Cache-Control: max-age=86400")
// Cache response for 24 hours
@GET("data")
suspend fun getData(): Response<DataModel>
}

By setting the appropriate caching headers, you can control how long responses are cached and under which conditions they are considered stale.

Conclusion

In this blog post, we discussed strategies to implement network calls with minimal performance impact in Kotlin-based Android apps. We explored choosing the right networking library, optimizing network calls, and implementing caching mechanisms. By following these best practices, you can ensure efficient network operations and deliver a smooth user experience in your Android applications.

Remember to continually monitor and profile your app's network performance to identify potential bottlenecks and areas for further optimization.

Happy coding!

USING ARKIT WITH SWIFT TO BUILD AR APPLICATIONS IN IOS

Published: · Last updated: · 5 min read
Appxiom Team
Mobile App Performance Experts

Augmented Reality (AR) has become an exciting technology that allows developers to create immersive experiences by overlaying virtual objects onto the real world. ARKit, Apple's framework for building AR applications, provides powerful tools and features to integrate AR into iOS apps using the Swift programming language.

In this blog post, we will explore how to use ARKit with Swift to create an AR application step by step.

Prerequisites

Before we dive into coding, make sure you have the following prerequisites:

  • A Mac running macOS 10.13.2 or later.

  • Xcode 9.0 or later.

  • An iOS device with an A9 or later processor, running iOS 11.0 or later.

  • Basic knowledge of Swift programming language and iOS app development.

Setting Up ARKit

To get started, let's create a new iOS project in Xcode and configure it for ARKit. Follow these steps:

  • Open Xcode and click on "Create a new Xcode project."

  • Choose "Augmented Reality App" template under the "App" category.

  • Enter the product name, organization identifier, and select Swift as the language.

  • Choose a location to save your project and click "Create."

Exploring the Project Structure

Once the project is created, let's take a quick look at the project structure:

  • AppDelegate.swift: The entry point of the application.

  • ViewController.swift: The default view controller for the ARKit app.

  • Main.storyboard: The user interface layout for the app.

  • Assets.xcassets: The asset catalog where you can add images and other resources.

  • Info.plist: The property list file that contains the configuration settings for the app.

Understanding the View Controller

The ViewController.swift file is the main view controller for our ARKit app. Open the file and let's explore its structure:

import UIKit
import ARKit

class ViewController: UIViewController {

@IBOutlet var sceneView: ARSCNView!

override func viewDidLoad() {
super.viewDidLoad()

// Set the view's delegate
sceneView.delegate = self
// Create a new scene
let scene = SCNScene()

// Set the scene to the view
sceneView.scene = scene
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// Create a session configuration
let configuration = ARWorldTrackingConfiguration()

// Run the view's session
sceneView.session.run(configuration)
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

// Pause the view's session
sceneView.session.pause()
}
}

extension ViewController: ARSCNViewDelegate {

}

The ViewController class inherits from UIViewController and conforms to the ARSCNViewDelegate protocol. It contains an ARSCNView object named sceneView, which is responsible for rendering the AR scene.

In the viewDidLoad() method, we set the sceneView delegate to self and create a new SCNScene object. We then assign the created scene to the sceneView.scene property.

In the viewWillAppear() method, we create an ARWorldTrackingConfiguration object, which is the primary configuration for AR experiences. We run the AR session by calling sceneView.session.run() with the created configuration.

Finally, in the viewWillDisappear() method, we pause the AR session by calling sceneView.session.pause().

Adding 3D Objects to the Scene

To add 3D objects to the AR scene, we need to implement the ARSCNViewDelegate methods. Modify the extension block in ViewController.swift as follows:

extension ViewController: ARSCNViewDelegate {

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// Check if the added anchor is an ARPlaneAnchor
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }

// Create a new plane node with the anchor's dimensions
let planeNode = createPlaneNode(with: planeAnchor)

// Add the plane node to the scene
node.addChildNode(planeNode)
}

private func createPlaneNode(with anchor: ARPlaneAnchor) -> SCNNode {
// Create a plane geometry with the anchor's dimensions
let planeGeometry = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z))

// Set the plane's color
planeGeometry.materials.first?.diffuse.contents = UIColor.blue.withAlphaComponent(0.5)

// Create a plane node with the geometry
let planeNode = SCNNode(geometry: planeGeometry)

// Position the plane node at the anchor's center
planeNode.position = SCNVector3(anchor.center.x, 0, anchor.center.z)

// Rotate the plane node to match the anchor's orientation
planeNode.eulerAngles.x = -.pi / 2
return planeNode
}
}

In the renderer(_:didAdd:for:) method, we check if the added anchor is an ARPlaneAnchor. If it is, we call the createPlaneNode(with:) method to create a plane node and add it to the scene.

The createPlaneNode(with:) method takes an ARPlaneAnchor as input and creates an SCNPlane geometry with the anchor's dimensions. We set the plane's color to blue with 50% transparency. Then, we create an SCNNode with the plane geometry, position it at the anchor's center, and rotate it to match the anchor's orientation. Finally, we return the plane node.

Running the AR App

Now that we have implemented the basic setup and added functionality to display plane nodes, let's run the AR app on a compatible iOS device. Follow these steps:

  • Connect your iOS device to your Mac.

  • Select your iOS device as the build destination in Xcode.

  • Click the "Play" button or press Command+R to build and run the app on your device.

Once the app is launched, point the camera at a flat surface, such as a tabletop or floor. As the ARKit detects and recognizes the surface, it will display a blue semi-transparent plane overlay on it.

Conclusion

In this blog post, we learned how to use ARKit with Swift to create an AR application in iOS. We explored the project structure, understood the view controller, and added 3D plane nodes to the scene using the ARSCNViewDelegate methods.

This is just the beginning of what you can achieve with ARKit. You can further enhance your AR app by adding custom 3D models, interactive gestures, and more.

Have fun exploring the possibilities of AR with Swift and ARKit!

Happy coding!

A GUIDE TO UTILIZING MACHINE LEARNING FEATURES OF FLUTTER

Published: · Last updated: · 4 min read
Appxiom Team
Mobile App Performance Experts

Machine learning is revolutionizing mobile app development, enabling intelligent decision-making and enhancing user experiences. Flutter, the open-source UI toolkit from Google, offers a robust set of tools and libraries to seamlessly integrate machine learning capabilities into your applications.

In this blog post, we will dive into the practical aspects of utilizing Flutter's machine learning features, accompanied by relevant code samples.

1. Understanding Machine Learning Capabilities of Flutter

Flutter provides various machine learning options, including TensorFlow Lite, ML Kit, and community packages. These options allow developers to integrate machine learning models into their Flutter apps, leveraging pre-trained models or building custom models tailored to specific use cases.

2. Using TensorFlow Lite with Flutter

TensorFlow Lite is a lightweight framework for deploying machine learning models on mobile and embedded devices.

Let's explore how to use TensorFlow Lite with Flutter:

2.1 Model Selection

Choose a pre-trained TensorFlow Lite model or build a custom model using TensorFlow. Convert the model to TensorFlow Lite format. TensorFlow Hub is a great resource for finding pre-trained models for tasks like image recognition or natural language processing.

2.2 Integration

Add the TensorFlow Lite dependency to your Flutter project's pubspec.yaml file:

dependencies:flutter:sdk: flutter
tflite: ^X.X.X
# Replace with the latest version

2.3 Model Loading

Load the TensorFlow Lite model into your Flutter app using the TensorFlow Lite Flutter package. You can load the model from an asset file or a remote location:

import 'package:tflite/tflite.dart';

// Load the TensorFlow Lite model
await Tflite.loadModel(
model: 'assets/model.tflite',
labels: 'assets/labels.txt',
);

2.4 Model Inference

Perform inference with the loaded TensorFlow Lite model using input data and receive predictions or results:

List<dynamic> inference = await Tflite.runModelOnImage(
path: 'path_to_image.jpg',
numResults: 5,
);

// Process the inference results
inference.forEach((result) {
final label = result['label'];
final confidence = result['confidence'];
print('Label: $label, Confidence: $confidence');
});

3. Leveraging ML Kit for Flutter

ML Kit is a suite of machine learning capabilities provided by Google, simplifying the integration of machine learning models into mobile apps. Let's see how to use ML Kit with Flutter:

3.1 Integration

Add the ML Kit Flutter package as a dependency to your pubspec.yaml file:

dependencies:flutter:sdk: flutter
firebase_ml_vision: ^X.X.X
# Replace with the latest version

3.2 Model Selection

Choose the ML Kit model that suits your application requirements. For example, to incorporate text recognition, use the Text Recognition API.

3.3 Model Configuration

Configure the ML Kit model by specifying parameters such as language support, confidence thresholds, and other options.

3.4 Integration and Inference

Integrate the model into your app and perform inference using the ML Kit Flutter package:

import 'package:firebase_ml_vision/firebase_ml_vision.dart';

// Initialize the text recognizer
final textRecognizer = FirebaseVision.instance.textRecognizer();

// Process an image and extract text
final FirebaseVisionImage visionImage = FirebaseVisionImage.fromFilePath('path_to_image.jpg');
final VisionText visionText = await textRecognizer.processImage(visionImage);

// Extract text from the VisionText object
final extractedText = visionText.text;

// Perform additional processing with the extracted text
// ...

4. Exploring Flutter Community Packages

In addition to TensorFlow Lite and ML Kit, the Flutter community has developed various packages providing machine learning functionalities. These packages cover areas like natural language processing, image processing, recommendation systems, etc. Popular community packages include tflite_flutter, flutter_tflite, and flutter_native_image.

5. Custom Machine Learning Models with Flutter

If the available pre-trained models do not meet your specific requirements, you can build custom machine learning models using TensorFlow or other frameworks. Once trained and optimized, convert your model to TensorFlow Lite format and integrate it into your Flutter app using the steps outlined in Section 2.

Conclusion

Flutter's machine learning capabilities empower developers to create intelligent and feature-rich mobile applications.

By leveraging TensorFlow Lite, ML Kit, or community packages, you can seamlessly integrate machine learning models into your Flutter apps. The provided code samples serve as a starting point for your exploration of Flutter's machine learning features, opening up a realm of possibilities for creating innovative and smart mobile applications.

TESTING KOTLIN BASED ANDROID APPS

Published: · Last updated: · 9 min read
Appxiom Team
Mobile App Performance Experts

Testing is an integral part of the software development process, and Android app development is no exception. Kotlin, being the official programming language for Android development, provides developers with powerful tools and frameworks for testing Android apps.

In this blog, we will explore various testing strategies and best practices for testing Kotlin Android apps, ensuring high-quality and robust applications.

1. Setting up the Testing Environment

Before diving into testing, you need to set up the testing environment for your Kotlin Android app. This involves adding the necessary dependencies and libraries and determining the types of tests you'll perform.

1.1. Dependencies and Libraries

To enable testing, include the following dependencies in your app's build.gradle file:

dependencies {
// Testing dependencies
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// Other dependencies...
}

1.2. Test Types

There are three main types of tests in Android app development:

  • Unit Tests: Focus on testing individual components in isolation, such as functions, classes, or modules.

  • Instrumented Tests: Run on an Android device or emulator and interact with the app's UI components and resources.

  • Automated UI Tests: Similar to instrumented tests but are written to simulate user interactions and test user flows automatically.

Now that the testing environment is set up let's move on to the different testing strategies.

2. Unit Testing

Unit testing involves testing individual components of your app in isolation to ensure that they function correctly.

2.1. Introduction to Unit Testing

Unit tests focus on testing small units of code, such as individual functions or classes. They help identify bugs early in the development process, improve code maintainability, and provide fast feedback during development.

2.2. Writing Unit Tests in Kotlin

To write unit tests in Kotlin, you can use the JUnit testing framework. Write test methods that assert the expected behavior of the code being tested.

For example, test a function that calculates the sum of two numbers:

import org.junit.Test
import org.junit.Assert.assertEquals

class MathUtilsTest {
@Test
fun testSum() {
val result = MathUtils.sum(2, 3)
assertEquals(5, result)
}
}

2.3. Using Mockito for Mocking Dependencies

Sometimes, unit tests require mocking dependencies to isolate the code being tested. Mockito is a popular mocking framework that simplifies the creation of mock objects. It allows you to define the behavior of mock objects and verify interactions with them.

For example:

import org.junit.Test
import org.junit.Assert.assertEquals
import org.mockito.Mockito.*

class UserManagerTest {
@Test
fun testUserCreation() {
val userService = mock(UserService::class.java)
val userManager = UserManager(userService)

`when`(userService.createUser("John Doe")).thenReturn(User("John Doe"))

val user = userManager.createUser("John Doe")

assertEquals("John Doe", user.name)
verify(userService).createUser("John Doe")
}
}

2.4. Running Unit Tests

To run unit tests, right-click on the test class or package in Android Studio and select "Run 'ClassName'" or "Run 'PackageName'." You can also use Gradle commands like ./gradlew test to run tests from the command line.

3. Instrumentation Testing

Instrumentation tests allow you to test your app's behavior on an Android device or emulator. These tests interact with the app's UI components, resources, and the Android framework.

3.1. Introduction to Instrumentation Testing

Instrumentation tests are essential for verifying the correct behavior of your app's UI and interactions with the underlying system. They help catch bugs related to UI rendering, user input handling, and inter-component communication.

3.2. Writing Instrumented Tests in Kotlin

To write an instrumented test in Kotlin, use the androidx.test framework. Create a test class and annotate it with @RunWith(AndroidJUnit4::class). Use the @Test annotation on individual test methods.

For example:

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityInstrumentedTest {
@Rule@JvmField
val activityRule = ActivityTestRule(MainActivity::class.java)

@Test
fun testButtonClick() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext

// Simulate a button click
onView(withId(R.id.button)).perform(click())

// Verify the expected text is displayed
onView(withId(R.id.textView)).check(matches(withText("Button Clicked")))
}
}

3.3. Running Instrumented Tests

To run instrumented tests, right-click on the test class or package in Android Studio and select "Run 'ClassName'" or "Run 'PackageName'." You can also use Gradle commands like ./gradlew connectedAndroidTest to run instrumented tests from the command line.

3.4. Interacting with UI Elements

The androidx.test.espresso library provides a fluent and expressive API for interacting with UI elements in instrumented tests. Use methods like onView, perform, and check to find views and perform actions on them.

For example, onView(withId(R.id.button)).perform(click()) simulates a click on a button with the specified ID.

3.5. Using Espresso for UI Testing

Espresso is a popular testing framework within androidx.test.espresso for UI testing. It simplifies writing concise and readable tests for Android UI components. Espresso provides a rich set of matchers, actions, and assertions.

For more details, visit the link provided at the end of this blog [1].

4. Automated UI Testing

Automated UI tests, also known as end-to-end tests, simulate user interactions and test user flows automatically. These tests ensure that different parts of the app work together correctly.

4.1. Introduction to Automated UI Testing

Automated UI tests simulate user interactions, such as button clicks, text input, and gestures, to test the app's behavior and flow. These tests help catch integration issues, data flow problems, and user experience regressions.

4.2. Writing Automated UI Tests in Kotlin

To write automated UI tests in Kotlin, you can use frameworks like Espresso or UI Automator. Create test classes and use the testing APIs to interact with UI elements and perform actions.

For example:

import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.junit.Test

class MainActivityAutomatedTest {
@Test
fun testButtonClick() {
ActivityScenario.launch(MainActivity::class.java)

// Simulate a button click
onView(withId(R.id.button)).perform(click())

// Verify the expected text is displayed
onView(withId(R.id.textView)).check(matches(withText("Button Clicked")))
}
}

4.3. Running Automated UI Tests

To run automated UI tests, follow the same process as running instrumented tests. Right-click on the test class or package in Android Studio and select "Run 'ClassName'" or "Run 'PackageName'." Use Gradle commands like ./gradlew connectedAndroidTest to run tests from the command line.

4.4. Testing Navigation and User Flows

Automated UI tests are ideal for testing navigation and user flows within your app. Simulate user interactions to move between screens, verify correct data flow, and validate the expected behavior at each step.

5. Test Doubles and Dependency Injection

Test doubles are objects used in place of real dependencies during testing. Dependency Injection (DI) helps manage dependencies and facilitates the use of test doubles.

5.1. Understanding Test Doubles

Test doubles include stubs, mocks, fakes, and dummies. They allow you to isolate code under test, simulate specific behaviors, and verify interactions. Use test doubles to replace external dependencies or collaborator objects.

5.2. Using Dependency Injection for Testability

Design your app with dependency injection principles in mind. Dependency injection frameworks like Dagger or Koin can help manage dependencies and make testing easier. Inject test doubles instead of real dependencies during testing.

5.3. Mocking Dependencies with DI Containers

DI containers, such as Mockito or Koin, provide mechanisms to define test-specific configurations and replace real dependencies with test doubles. Use these containers to inject mock objects and stub behaviors.

5.4. Configuring Test-Specific Dependencies

Configure your DI container to provide test-specific dependencies when running tests. This allows you to control the behavior of dependencies during testing and ensure predictable test results.

6. Test Coverage and Continuous Integration

Test coverage measures the extent to which your code is tested by your test suite. Continuous Integration (CI) ensures that your tests are run automatically and regularly as part of your development workflow.

6.1. Measuring Test Coverage

Use tools like JaCoCo or Android Studio's built-in code coverage to measure test coverage. Aim for high code coverage to ensure that critical parts of your app are adequately tested.

6.2. Configuring Continuous Integration (CI)

Set up a CI system, such as Jenkins, Travis CI, or CircleCI, to automatically build and test your app. Configure your CI pipeline to run your tests and generate test reports.

6.3. Running Tests on CI Platforms

Configure your CI system to execute your tests during the build process. Ensure that your build script or CI configuration includes the necessary commands to run unit tests, instrumented tests, and automated UI tests.

7. Using APM Tools

APM tools play a crucial role in monitoring and analyzing the performance and stability of your Kotlin Android apps. They provide real-time insights into crashes, errors, and performance bottlenecks, helping you identify and resolve issues quickly.

Some of the popular APM tools for Android apps are Bugsnag, Appxiom, New Relic and Sentry.

8. Testing Best Practices

Follow these best practices to write effective and maintainable tests for your Kotlin Android apps:

8.1. Isolating Tests

Each test should be independent and not rely on the state or side effects of other tests. Isolate tests to prevent dependencies between them, ensuring consistent and reliable results.

8.2. Writing Readable and Maintainable Tests

Write tests that are easy to understand and maintain. Use descriptive method and variable names, organize tests logically, and avoid duplicating code across tests.

8.3. Using Test Fixtures

Test fixtures are preconditions or shared resources required for multiple tests. Use setup and teardown methods, annotations, or test fixture classes to set up common test conditions and clean up resources.

8.4. Test-Driven Development (TDD)

Consider Test-Driven Development as a development practice. Write tests before implementing functionality. This approach helps define the desired behavior, ensures testability, and provides quick feedback.

8.5. Performance Testing

Consider performance testing to identify bottlenecks and optimize critical parts of your app. Measure performance metrics, such as response times or memory usage, to ensure your app meets performance expectations.

8.6. Edge Cases and Boundary Testing

Test edge cases and boundary conditions, such as maximum and minimum input values or error scenarios. These tests help uncover potential issues related to limits, constraints, or exceptional situations.

Conclusion

In this blog, we explored various testing strategies for Kotlin Android apps. We covered unit testing, instrumentation testing, automated UI testing, test doubles, dependency injection, test coverage, continuous integration, APM tools, and best practices.

By incorporating these testing strategies into your development process, you can ensure high-quality, robust, and reliable Kotlin Android apps. Remember to continuously iterate and improve your test suite to catch bugs early and deliver exceptional user experiences.

  • Testing Jetpack Compose based Android UI using Espresso.

USING REALM DATABASE IN IOS SWIFT APPS

Published: · Last updated: · 5 min read
Appxiom Team
Mobile App Performance Experts

Realm is a popular mobile database solution that provides an alternative to traditional SQLite databases in iOS apps. It offers a simple and efficient way to persist data locally on the device and perform complex queries and transactions.

In this blog, we will explore how to integrate and use Realm in iOS apps to manage data storage and retrieval.

Prerequisites

To follow along with this tutorial, you should have a basic understanding of iOS app development using Swift and Xcode. Additionally, ensure that you have Xcode installed on your development machine.

Step 1: Installing Realm

To start using Realm in your iOS app, you need to install the RealmSwift library. There are multiple ways to install Realm, but the recommended method is using CocoaPods, a dependency manager for iOS projects.

Follow these steps to install Realm using CocoaPods:

  1. Open Terminal and navigate to your project directory.

  2. If you haven't already initialized your project with CocoaPods, run the command: pod init. This will create a Podfile for your project.

  3. Open the Podfile using a text editor and add the following line inside the target block:

pod 'RealmSwift'
  1. Save the Podfile and run the command: pod install in Terminal.

  2. Wait for CocoaPods to download and install the RealmSwift library. Once completed, close your Xcode project and open the newly generated .xcworkspace file.

Step 2: Setting up Realm in your project

After installing Realm, you need to configure it in your iOS project. Follow these steps to set up Realm in your app:

  1. In Xcode, open your project's .xcworkspace file.

  2. Create a new Swift file (e.g., RealmManager.swift) to manage your Realm configuration and interactions.

  3. Import the RealmSwift library at the top of the file:

import RealmSwift
  1. Declare a class named RealmManager and add the following code:
final class RealmManager {
static let shared = RealmManager() // Singleton instance
private let realm: Realm
private init() {
// Get the default Realm configuration
guard let realm = try? Realm() else {
fatalError("Failed to initialize Realm")
}
self.realm = realm
}
}

Step 3: Creating a Realm Object

In Realm, data is organized into objects, similar to tables in a traditional database. Each Realm object represents a row in the database table.

Follow these steps to create a Realm object in your iOS app:

  1. Create a new Swift file (e.g., Task.swift) to define your Realm object.

  2. Import the RealmSwift library at the top of the file:

import RealmSwift
  1. Declare a new class and inherit from the Object class provided by Realm:
final class Task: Object {
@Persisted(primaryKey: true) var id: ObjectId // Primary key
@Persisted var name: String = ""
@Persisted var dueDate: Date?
}
  1. Customize the properties and their types according to your app's requirements. The @Persisted attribute marks a property for persistence in the Realm database.

Step 4: Performing CRUD Operations

Now that you have set up Realm and defined a Realm object, you can perform CRUD (Create, Read, Update, Delete) operations on your data. Follow these steps to perform basic CRUD operations:

  1. To add a new object to the Realm database, use the following code:
let task = Task()
task.name = "Sample Task"
task.dueDate = Date()

try? RealmManager.shared.realm.write {
RealmManager.shared.realm.add(task)
}
  1. To fetch all objects of a specific type, use the following code:
let tasks = RealmManager.shared.realm.objects(Task.self)
for task in tasks {
print("Task Name: \(task.name)")
print("Due Date: \(task.dueDate ?? "")")
}
  1. To fetch an object by its id, use the following code:
func fetchTaskById(id: ObjectId) -> Task? {
return RealmManager.shared.realm
.object(ofType: Task.self, forPrimaryKey: id)
}
  1. To fetch objects by name, use the following code:
func fetchTasksByName(name: String) -> Results<Task>? {
let predicate = NSPredicate(format: "name == %@", name
return RealmManager.shared.realm
.objects(Task.self).filter(predicate)
}
  1. To update an existing object, modify its properties and save the changes:
if let task = tasks.first {
try? RealmManager.shared.realm.write {
task.name = "Updated Task"
}
}
  1. To delete an object from the Realm database, use the following code:
if let task = tasks.first {
try? RealmManager.shared.realm.write {
RealmManager.shared.realm.delete(task)
}
}

Step 5: Advanced Realm Features

Realm offers additional features to handle more complex scenarios. Here are a few examples:

  1. Relationships: You can establish relationships between Realm objects using properties like LinkingObjects or RealmOptional. Refer to the Realm documentation for detailed examples.

  2. Queries: Realm provides a powerful query API to fetch objects based on specific criteria. For example:

let overdueTasks = RealmManager.shared.realm.objects(Task.self).filter("dueDate < %@", Date())
  1. Notifications: You can observe changes in Realm objects using notifications. This allows your app to stay updated with real-time changes made by other parts of the app or remote data sources. Refer to the Realm documentation for more information.

Conclusion

In this blog, we explored the basics of using Realm in iOS apps. We learned how to install Realm, set it up in our project, create Realm objects, and perform CRUD operations. We also briefly touched upon advanced features such as relationships, queries, and notifications.

Realm provides a robust and efficient solution for data persistence in iOS apps, offering a wide range of features to simplify database management. Feel free to explore the Realm documentation for more in-depth usage and examples.

Happy coding !

GUIDE TO IMPLEMENT CONTINUOUS INTEGRATION (CI) AND CONTINUOUS DELIVERY (CD) FOR FLUTTER APPS

Published: · Last updated: · 6 min read
Appxiom Team
Mobile App Performance Experts

In today's fast-paced software development landscape, it is crucial to adopt practices that enable rapid and efficient delivery of high-quality mobile applications. Continuous Integration (CI) and Continuous Delivery (CD) are two essential methodologies that help streamline the development, testing, and deployment processes.

In this blog, we will explore how to implement CI/CD for Flutter apps, leveraging popular tools like Jenkins and Fastlane.

What is Continuous Integration (CI)?

Continuous Integration is a software development practice that involves regularly merging code changes from multiple developers into a shared repository. The primary goal of CI is to detect and address integration issues early in the development cycle. With CI, developers continuously integrate their changes into the main branch, triggering an automated build and testing process to ensure that the application remains functional.

What is Continuous Delivery (CD)?

Continuous Delivery extends CI by automating the entire release process. It focuses on delivering software that is always in a releasable state, making it ready for deployment to any environment at any time. CD includes activities like automated testing, packaging, and deployment, ensuring that the application can be easily released to production or other target environments.

Setting Up CI/CD for Flutter Apps

Step 1: Setting up Jenkins

  • Install Jenkins: Install Jenkins on a server or use a hosted Jenkins service, following the installation instructions provided by the Jenkins documentation.

  • Install Required Plugins: Set up Jenkins with necessary plugins such as Git, Flutter, and Fastlane. Navigate to the Jenkins dashboard, go to "Manage Jenkins" -> "Manage Plugins," and search for the required plugins. Install and restart Jenkins after plugin installation.

  • Configure Flutter SDK Path: Configure the Flutter SDK path in the Jenkins global configuration. Navigate to "Manage Jenkins" -> "Global Tool Configuration" and locate the Flutter section. Provide the path to the Flutter SDK installation directory.

Step 2: Creating a Jenkins Pipeline

  • Create a New Pipeline Project: On the Jenkins dashboard, click on "New Item" and select "Pipeline" to create a new pipeline project.

  • Define Pipeline Script: In the pipeline configuration, define the pipeline script, which includes stages for building, testing, and deploying the Flutter app. Use the Flutter CLI commands within the pipeline script to run tests, build APKs or iOS artifacts, and generate necessary files.

Step 3: Integrating Fastlane

  • Install Fastlane: Install Fastlane using RubyGems by running the command gem install fastlane in your command-line interface.

  • Configure Fastlane: Configure Fastlane to handle the automation of code signing, distribution, and other CD tasks for Flutter apps. Navigate to your Flutter project directory and run fastlane init to set up Fastlane in your project.

  • Define Fastlane Lanes: Define Fastlane lanes for different stages of the CD process, such as beta testing, app store deployment, etc. Modify the generated Fastfile to include the necessary lanes and their respective actions.

Step 4: Configuring Version Control and Hooks

  • Connect to Version Control System: Connect your Flutter project to a version control system like Git. Initialize a Git repository in your project directory, commit the initial codebase, and set up the remote repository.

  • Set Up Git Hooks: Set up Git hooks to trigger the Jenkins pipeline on code commits or merges. Create a post-commit or post-merge hook in your local Git repository's .git/hooks directory, invoking a command that triggers the Jenkins pipeline when changes are pushed to the repository.

  • Configure Webhook Notifications: Configure webhook notifications in your version control system to receive build status updates. Set up the webhook URL in your Git repository's settings to notify Jenkins of new code changes.

Step 5: Testing and Building the Flutter App

  • Add Tests to Your Flutter Project: Add unit tests and integration tests to your Flutter project using Flutter's built-in testing framework or any preferred testing library.

  • Configure Jenkins Pipeline for Testing: Modify the Jenkins pipeline script to execute the tests during the CI process. Use Flutter CLI commands like flutter test to run the tests and generate test reports.

  • Track Test Coverage: Utilize code coverage tools like lcov to measure test coverage in your Flutter project. Generate coverage reports and integrate them into your CI/CD pipeline for tracking the test coverage over time.

Step 6: Deployment and Distribution

  • Configure Fastlane Lanes for Deployment Targets: Configure Fastlane lanes for different deployment targets, such as Google Play Store or Apple App Store. Modify the Fastfile to include actions for building and distributing the Flutter app to the desired platforms.

  • Define Deployment Configurations: Define deployment-related configurations such as code signing identities, release notes, and versioning in the Fastfile.

  • Deploying the Flutter App: Execute the Fastlane lanes to build and distribute the Flutter app to the target environments. Use the appropriate Fastlane commands like fastlane deploy to trigger the deployment process.

Sample files

Jenkins Pipeline Script (Jenkinsfile):

pipeline {
agent any

stages {
stage('Checkout') {
steps {
// Checkout source code from Git repository
git 'https://github.com/your-repo/flutter-app.git'
}
}

stage('Build') {
steps {
// Install Flutter dependencies
sh 'flutter pub get'

// Build the Flutter app for Android
sh 'flutter build apk --release'

// Build the Flutter app for iOS
sh 'flutter build ios --release --no-codesign'
}
}

stage('Test') {
steps {
// Run unit tests
sh 'flutter test'
}
}

stage('Deploy') {
steps {
// Install Fastlane
sh 'gem install fastlane'

// Run Fastlane lane for deployment
sh 'fastlane deploy'
}
}
}
}

Fastfile:

default_platform(:ios)

platform :ios do
lane :deploy do
# Match code signing
match(
type: "appstore",
readonly: true,
keychain_name: "fastlane_tmp_keychain",
keychain_password: "your-password"
)

# Build and distribute the iOS app
gym(
scheme: "YourAppScheme",
export_method: "app-store"
)
end
end

platform :android do
lane :deploy do
# Build and distribute the Android app
gradle(
task: "assembleRelease"
)

# Upload the APK to Google Play Store
playstore_upload(
track: "internal",
apk: "app/build/outputs/apk/release/app-release.apk",
skip_upload_metadata: true,
skip_upload_images: true
)
end
end

Note: Remember to update the Jenkins pipeline script and Fastfile according to your specific project configurations, such as repository URLs, app names, code signing identities, and deployment targets.

Ensure that you have the necessary dependencies and configurations in place, such as Flutter SDK, Fastlane, and code signing certificates, before executing the pipeline.

This sample provides a basic structure for CI/CD with Jenkins and Fastlane for Flutter apps. You can further customize and enhance these scripts to meet your project's requirements.

Conclusion

Implementing Continuous Integration and Continuous Delivery for Flutter apps brings significant benefits to the development and deployment processes. By automating the build, testing, and deployment stages, developers can save time, reduce errors, and ensure the consistent delivery of high-quality applications. Jenkins and Fastlane provide powerful tools for achieving CI/CD in Flutter projects, allowing developers to focus on building exceptional mobile experiences.

By adopting CI/CD practices, Flutter developers can accelerate their development cycles, improve collaboration, and deliver reliable apps to end-users more efficiently.

Remember, CI/CD is an iterative process, and it's crucial to continuously improve and adapt your workflows to meet your project's evolving needs.

Happy coding and deploying your Flutter apps with CI/CD!

INTEGRATING SWIFTUI AND UIKIT: BEST PRACTICES AND MIGRATION TIPS

Published: · Last updated: · 6 min read
Don Peter
Cofounder and CTO, Appxiom

As an iOS developer, the introduction of SwiftUI has brought exciting opportunities for building dynamic and interactive user interfaces. However, many projects still rely on UIKit, the framework that has been the foundation of iOS app development for years.

In this blog post, we will explore best practices and migration tips for integrating SwiftUI and UIKit, allowing developers to leverage the strengths of both frameworks seamlessly.

Understanding SwiftUI and UIKit

SwiftUI, introduced with iOS 13, offers a declarative approach to building user interfaces. It allows developers to describe the desired UI state, and SwiftUI automatically updates the views accordingly. On the other hand, UIKit, the older imperative framework, provides a more granular control over the user interface.

Best Practices for Integration

Modular Approach

To achieve a smooth integration, it is advisable to adopt a modular approach. Consider encapsulating SwiftUI views and UIKit components into separate modules or frameworks. This allows for easier management and separation of concerns.

SwiftUI as a Container

SwiftUI can act as a container for UIKit views, enabling a gradual migration. By wrapping UIKit components with SwiftUI's UIViewRepresentable protocol, you can seamlessly incorporate UIKit into SwiftUI views.

import SwiftUI
import UIKit

// UIKit View
class MyUIKitView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}

private func setupUI() {
backgroundColor = .green

let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 50))
label.text = "This is a UIKit view"
label.textAlignment = .center
label.center = center
addSubview(label)
}
}

// SwiftUI Container View
struct SwiftUIContainerView: UIViewRepresentable {
func makeUIView(context: Context) -> MyUIKitView {
return MyUIKitView()
}

func updateUIView(_ uiView: MyUIKitView, context: Context) {
// Update the view if needed
}
}

// SwiftUI ContentView
struct ContentView: View {
var body: some View {
VStack {
Text("Welcome to SwiftUI Container")
.font(.title)
.foregroundColor(.blue)

SwiftUIContainerView()
.frame(width: 250, height: 250)
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

In this code snippet, we have a MyUIKitView class, which is a custom UIView subclass representing a UIKit view. It sets up a simple green background and adds a UILabel as a subview.

The SwiftUIContainerView is a UIViewRepresentable struct that acts as a bridge between the SwiftUI and UIKit worlds. It conforms to the protocol by implementing the makeUIView function, where it creates and returns an instance of MyUIKitView.

The ContentView is a SwiftUI view that utilizes the SwiftUIContainerView by embedding it within a VStack. It also displays a welcome message using a Text view.

By using SwiftUIContainerView, you can seamlessly incorporate UIKit views within your SwiftUI-based projects, allowing for a gradual migration from UIKit to SwiftUI or the combination of both frameworks.

Hosting UIKit in SwiftUI

Conversely, you can use SwiftUI's UIViewControllerRepresentable protocol to host SwiftUI views within UIKit-based projects. This way, you can gradually introduce SwiftUI elements into existing UIKit apps.

Data Sharing

Establishing a smooth data flow between SwiftUI and UIKit is essential. You can leverage frameworks like Combine or NotificationCenter to share data and propagate changes between the two frameworks.

import SwiftUI
import UIKit
import Combine

// Shared Data Model
class SharedData: ObservableObject {
@Published var value: String = ""

// Example function to update the value
func updateValue(_ newValue: String) {
value = newValue
}
}

// Example UIKit View Controller
class MyUIKitViewController: UIViewController {
var sharedData: SharedData!
private var cancellables = Set<AnyCancellable>()

override func viewDidLoad() {
super.viewDidLoad()

let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 50))
label.textAlignment = .center
label.center = view.center
view.addSubview(label)

// Observe changes in sharedData's value using Combine
sharedData.$value
.sink { [weak self] newValue in
label.text = newValue
}
.store(in: &cancellables)
}
}

// SwiftUI View Hosting UIKit View Controller
struct SwiftUIHostingUIKitView: UIViewControllerRepresentable {
typealias UIViewControllerType = MyUIKitViewController
let sharedData: SharedData

func makeUIViewController(context: Context) -> MyUIKitViewController {
let viewController = MyUIKitViewController()
viewController.sharedData = sharedData
return viewController
}

func updateUIViewController(_ uiViewController: MyUIKitViewController, context: Context) {
// Update the hosted UIKit view controller if needed
}
}

// SwiftUI ContentView
struct ContentView: View {
@StateObject private var sharedData = SharedData()

var body: some View {
VStack {
Text("Welcome to SwiftUI Data Sharing")
.font(.title)
.foregroundColor(.blue)

SwiftUIHostingUIKitView(sharedData: sharedData)
.frame(width: 250, height: 250)

TextField("Enter a value", text: $sharedData.value)
.padding()
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

In this code snippet, we have a SharedData class that acts as a shared data model between SwiftUI and UIKit. It uses ObservableObject and Published property wrapper from Combine to make the value property observable.

The MyUIKitViewController is a custom UIViewController subclass representing a UIKit view controller. It observes changes in the shared data's value property using Combine, and updates the UILabel accordingly.

The SwiftUIHostingUIKitView is a UIViewControllerRepresentable struct that hosts the MyUIKitViewController within SwiftUI. It passes the shared data object to the UIKit view controller using the sharedData property.

The ContentView is a SwiftUI view that creates an instance of SharedData as a @StateObject. It embeds the SwiftUIHostingUIKitView, allowing the shared data to be accessed and updated from both the SwiftUI TextField and the UIKit view controller.

By using Combine and the ObservableObject protocol, you can establish data sharing between SwiftUI and UIKit components, ensuring that changes made in one framework are propagated and reflected in the other.

Migration Tips

  • Start with New Features: When migrating from UIKit to SwiftUI, it's often best to start with new features or smaller isolated parts of your app. This approach minimizes the impact on existing code while allowing you to explore the capabilities of SwiftUI.

  • UIKit and SwiftUI Hybrid: Consider creating hybrid screens where you combine elements from both frameworks. This approach allows you to leverage SwiftUI's flexibility while preserving UIKit's existing codebase.

  • UIKit View Controllers: Reusing existing UIKit view controllers in SwiftUI can be accomplished by creating wrapper views conforming to UIViewControllerRepresentable. This approach allows you to incrementally migrate the UI layer to SwiftUI.

  • Understand SwiftUI's Layout System: SwiftUI has a unique layout system based on stacks, spacers, and modifiers. Take the time to understand and embrace this system to maximize the benefits of SwiftUI's responsive UI design.

  • Testing and Debugging: During the migration process, it is crucial to thoroughly test and debug your code. SwiftUI provides a live preview feature that facilitates real-time feedback, making it easier to identify and fix issues efficiently.

Conclusion

Integrating SwiftUI and UIKit opens up a world of possibilities for iOS developers. By following best practices and migration tips, you can smoothly transition between the two frameworks, harnessing the power of SwiftUI's declarative syntax and UIKit's extensive ecosystem.

Remember, the migration process may require careful planning and incremental changes, but the result will be a more efficient, modern, and delightful user experience. Embrace the best of both worlds and embark on your journey to create stunning iOS applications.

A COMPREHENSIVE GUIDE ON HOW TO TEST FLUTTER MOBILE APPS

Published: · Last updated: · 6 min read
Appxiom Team
Mobile App Performance Experts

In the fast-paced world of mobile app development, ensuring the quality and reliability of your application is crucial. Flutter, a popular cross-platform framework developed by Google, has gained significant traction among developers for its ability to create stunning mobile apps for both Android and iOS platforms. Testing plays a vital role in delivering a successful Flutter app, ensuring its functionality, performance, and user experience.

In this blog post, we will explore the different aspects of testing Flutter mobile apps and provide a comprehensive guide to help you achieve a robust and reliable application.

Understanding Flutter Testing Fundamentals

Before diving into the testing process, it's essential to familiarize yourself with the basic testing concepts in Flutter.

Flutter provides several testing frameworks and tools, including unit testing, widget testing, and integration testing. Understanding these concepts will allow you to choose the appropriate testing approach based on your application's requirements.

1. Writing Unit Tests

Unit tests are the foundation of any test suite and focus on testing individual units of code. In Flutter, you can use the built-in test package, which provides utilities for writing and executing unit tests. Unit tests help validate the behavior of functions, classes, and methods in isolation, ensuring that they produce the expected output for a given input.

Let's take a look at an example of a unit test:

import 'package:test/test.dart';

int sum(int a, int b) {
return a + b;
}

void main() {
test('Sum function adds two numbers correctly', () {
expect(sum(2, 3), equals(5));
expect(sum(0, 0), equals(0));
expect(sum(-1, 1), equals(0));
});
}

In this example, we define a sum function that adds two numbers. We then write a unit test using the test function from the test package. The expect function is used to assert that the actual result of the sum function matches the expected result.

2. Widget Testing

Widget testing in Flutter involves testing the UI components of your application. It allows you to verify if the widgets render correctly and behave as expected. The Flutter framework provides the flutter_test package, which offers a rich set of APIs for widget testing. With widget testing, you can simulate user interactions, verify widget states, and test widget rendering across different screen sizes and orientations.

Here's an example of a widget test:

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

void main() {
testWidgets('Button changes text when pressed', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: ElevatedButton(
onPressed: () {},
child: Text('Button'),
),
),
));

expect(find.text('Button'), findsOneWidget);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();

expect(find.text('Button Pressed'), findsOneWidget);
});
}

In this example, we create a widget test using the testWidgets function from the flutter_test package. We use the pumpWidget function to build and display the widget hierarchy. Then, we use the find function to locate the widget we want to interact with, and the tap function to simulate a tap on the widget. Finally, we assert that the widget's text changes to 'Button Pressed' after the tap.

3. Integration Testing

Integration testing focuses on testing the interaction between multiple components of your application, such as different screens, databases, APIs, and external dependencies. Flutter provides a powerful testing framework called Flutter Driver, which allows you to write integration tests that interact with your app as if a real user were using it. Integration tests help identify issues related to navigation, data flow, and interactions between different parts of your app.

Here's an example of an integration test:

import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
FlutterDriver driver;

setUpAll(() async {
driver = await FlutterDriver.connect();
});

tearDownAll(() async {
if (driver != null) {
driver.close();
}
});

test('Login and navigate to home screen', () async {
await driver.tap(find.byValueKey('username_field'));
await driver.enterText('john_doe');
await driver.tap(find.byValueKey('password_field'));
await driver.enterText('password123');
await driver.tap(find.byValueKey('login_button'));

await driver.waitFor(find.byValueKey('home_screen'));
});
}

In this example, we use the flutter_driver package to write an integration test. We set up a connection to the Flutter driver using the FlutterDriver.connect method. Then, we define a test that simulates a login flow by interacting with various widgets using the tap and enterText methods. Finally, we assert that the home screen is successfully displayed.

Test-Driven Development (TDD)

Test-Driven Development is a software development approach that emphasizes writing tests before writing the actual code. With TDD, you define the desired behavior of your app through tests and then write code to fulfill those test requirements. Flutter's testing tools and frameworks integrate seamlessly with TDD practices, making it easier to build reliable and maintainable applications. By writing tests first, you ensure that your code is thoroughly tested and behaves as expected.

Continuous Integration and Delivery (CI/CD)

Incorporating a robust CI/CD pipeline for your Flutter app is crucial to automate the testing process and ensure consistent quality across different stages of development. Popular CI/CD platforms like Jenkins, CircleCI, and GitLab CI/CD can be integrated with Flutter projects to run tests automatically on every code commit or pull request.

Additionally, you can leverage tools like Firebase Test Lab to test your app on various physical and virtual devices, ensuring compatibility and performance across different configurations.

Using Tools for Testing

Using tools like Firebase, Instabug, BugSnag and Appxiom to detect performance issues and other bugs will help you in detecting bugs which may otherwise go undetected in manual testing. They provide detailed bug reports with data that will help you to reproduce the bug and identify the root cause.

Conclusion

Testing is an integral part of the Flutter app development process, ensuring that your app functions as intended and delivers an excellent user experience. By following the practices outlined in this comprehensive guide and using the provided code samples, you can build a solid testing strategy for your Flutter mobile apps.

Remember to invest time in writing unit tests, widget tests, and integration tests, and consider adopting test-driven development practices. Furthermore, integrating your testing efforts with a reliable CI/CD pipeline will help you maintain a high level of quality and efficiency throughout the development lifecycle.

Last but not the least, use tools like Firebase, Instabug, BugSnag and Appxiom to detect performance issues and bugs.

Happy testing!