Skip to main content

137 posts tagged with "Apps"

View All Tags

UPDATES FOR IOS DEVELOPERS IN THE EU: INTRODUCING WEB DISTRIBUTION

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

Apple is bringing more options for distributing your apps in the EU due to Digital Markets Act (DMA). Whether you’re a seasoned developer or just starting out, these changes are positioned as a way to reach more users and enhance your app distribution strategies.

Developers have the option to stick with the existing App Store business terms or opt for the new terms tailored for iOS apps in the EU. Under the new EU business terms, developers can decide to distribute iOS apps within the EU through the App Store, other alternative app marketplaces or through own hosting.

Alternative distribution channels

Following are the two additional methods for developers to distribute iOS apps in EU along with App store distribution,

  • Third party App Marketplaces: Now, third party marketplaces have the option to offer a catalog of apps. This opens up new avenues for app discovery and distribution.

  • Linking out to Purchase: Developers can now choose how to design promotions, discounts, and other deals when directing users to complete a transaction for digital goods or services on an external webpage. The Apple-provided design templates are now optional, giving you more control over your promotional strategies.

Introducing Web Distribution for iOS

One of the most exciting updates is the introduction of Web Distribution, which allows authorized developers to distribute their iOS apps directly from their own website. Here’s what you need to know,

  • With Web Distribution, you can distribute your iOS apps to EU users directly from your website, giving you more control over the distribution process.

  • Apple will provide access to APIs that facilitate app distribution from the web, integrate with system functionality, back up and restore users’ apps.

  • Apps offered through Web Distribution must meet Notarization requirements just like in macOS apps to protect platform integrity, ensuring a secure experience for users. You can read more on notarization requirements here https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution

Eligibility and Requirements

To be eligible for Web Distribution, developers must meet specific criteria and commit to ongoing requirements to protect users. Here’s what you need to know,

  • Developers must be enrolled in the Apple Developer Program as an organization incorporated, domiciled, and/or registered in the EU.

  • Must be a member of Good Standing in the Apple Developer Program for Two Continuous Years or More,

This means that developers need to maintain their membership in the Apple Developer Program for at least two consecutive years without any significant issues or violations.

  • It ensures that developers have been actively engaged with the program and have adhered to its terms and conditions for a substantial period.

  • App with More Than One Million First Annual Installs on iOS in the EU in the Prior Calendar Year,

This refers to the number of initial installations (installs) of an app on iOS devices in the European Union during a single year. The app must have achieved more than one million first annual installs specifically within the EU region during the previous calendar year.

  • The requirement doesn't state that the app must consistently maintain one million installs each year to remain in the program.

  • Developers must agree to various terms, including offering apps only from their developer account, being responsive to communications from Apple, publishing transparent data collection policies, and following applicable laws.

Payments, Fees, and Taxes

We want to ensure that developers have clarity on payments, fees, and taxes associated with Web Distribution. Here’s what you need to know,

  • A Core Technology Fee (CTF) will be charged by apple.

What is the Core Technology Fee (CTF)?

It's a fee that developers pay to Apple.

  • It shows appreciation for the tools and support Apple offers to developers.

  • How is it calculated?

Developers pay €0.50 for first annual installs over one million in the past 12 months.

If a user has installed the app in multiple devices it will be treated as a single install only.

  • If your app has more than one million installs in a year, you pay this fee for each additional install beyond that.

  • Why does Apple charge this fee?

It helps Apple to cover the costs of maintaining and improving the tools and services that developers use.

  • It supports ongoing investments in technology to benefit developers and users alike.

  • Nonprofit organizations, accredited educational institutions, or government entities based in the EU that have been approved for a fee waiver are exempt from the Apple Developer Program annual membership fee and the Core Technology Fee.

  • Developers are responsible for collecting, reporting, and remitting any required tax to the appropriate tax authorities for transactions that take place using Web Distribution.

GRADLE FLAVORS: BUILDING MULTIPLE ANDROID APP VARIANTS WITH SINGLE CODEBASE

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

Gradle Flavors, also known as product flavors, allow developers to create multiple variants of their app within a single codebase. Each flavor can have its own unique configuration, resources, and dependencies, enabling customization based on factors such as branding, feature sets, or target audiences.

If you're developing free and paid versions of your app, adapting it for different languages or regions, or incorporating variations for testing purposes, Gradle Flavors offer unparalleled flexibility.

Why Use Gradle Flavors?

  • Customization: With Gradle Flavors, developers can easily tailor their app to suit specific user segments or market requirements. This level of customization fosters better user engagement and satisfaction.

  • Efficiency: Rather than maintaining separate codebases for different app variants, Gradle Flavors streamline the development process by centralizing code while allowing for variant-specific configurations. This results in reduced complexity and faster iteration cycles.

  • Consistency: By defining variant-specific resources and dependencies within the Gradle build script, developers ensure consistency across different app versions while minimizing the risk of errors or inconsistencies.

  • Market Segmentation: For businesses targeting diverse demographics or regions, Gradle Flavors facilitate the creation of specialized versions of the app tailored to each market segment's preferences and needs.

Free and Pro Versions

Let's illustrate the power of Gradle Flavors with the above scenario – creating free and pro versions of an app. Suppose you have an app called "WeatherApp" and want to offer both a free version with basic features and a paid pro version with additional functionalities.

android {
...
productFlavors {
free {
dimension "tier"
applicationId "com.example.weather.free"
versionCode 1
versionName "1.0"
// Define flavor-specific configurations
buildConfigField "boolean", "IS_PRO_VERSION", "false"
}
pro {
dimension "tier"
applicationId "com.example.weather.pro"
versionCode 1
versionName "1.0"
// Define flavor-specific configurations
buildConfigField "boolean", "IS_PRO_VERSION", "true"
}
}

buildTypes {
debug {
// Debug-specific configurations
...
}
release {
// Release-specific configurations
...
}
}
...
}

In this example, we define two product flavors: 'free' and 'pro', each with its own unique applicationID. We can then customize the behavior, features, and resources specific to each flavor, such as limiting certain features to the pro version or displaying different branding elements.

In the above example, we define two build types. "debug" and "release," each with its own configurations. These configurations might include signing configurations, proguard rules, or other build-specific settings.

With the flavors and build variants defined, Gradle will generate the following build variants:

  • freeDebug

  • freeRelease

  • proDebug

  • proRelease

Developers can then use these variants to build and test different versions of the app. For instance, they can build the "pro" release variant to generate a signed APK for distribution to users who have purchased the pro version of the app. Similarly, they can build the "free" debug variant to test new features or changes specific to the free version of the app.

Android Project Structure with Gradle Flavors

In the above example, the folder structure for the Android project would typically look like this:

- app
- src
- free
- java
- com
- example
- weather
- MainActivity.java
- ...
- res
- layout
- drawable
- values
- ...
- pro
- java
- com
- example
- weather
- MainActivity.java
- ...
- res
- layout
- drawable
- values
- ...
- main
- java
- com
- example
- weather
- MainActivity.java
- ...
- res
- layout
- drawable
- values
- ...

Here's a breakdown of the folder structure:

  • app: This is the main module of the Android project.

  • src: This directory contains the source code and resources for different build variants.

  • free: This directory contains the source code and resources specific to the "free" flavor.

java: Java source code files for the "free" flavor.

  • com.example.weather: Package directory.

  • MainActivity.java: Example activity class.

  • Other Java files specific to the "free" flavor.

  • res: Resource directory for the "free" flavor.

  • layout: XML layout files.

  • drawable: Image resources.

  • values: Resource files such as strings, colors, dimensions, etc.

  • Other resource directories specific to the "free" flavor.

  • pro: This directory contains the source code and resources specific to the "pro" flavor. The structure is similar to the "free" flavor but with resources and code specific to the "pro" variant.

  • main: This directory contains the main source code and resources shared among all flavors and build types. It serves as the base for all variants and contains code and resources common to both "free" and "pro" flavors.

By organizing the source code and resources in this way, Gradle can easily build different variants of the app by combining the contents of the "main" directory with the specific contents of each flavor directory (free and pro).

Leveraging Gradle flavors and build variants empowers Android developers to efficiently manage and customize multiple versions of their apps, catering to diverse user preferences and market requirements while maintaining codebase integrity and flexibility.

INTEGRATING URL_LAUNCHER IN FLUTTER APPS

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

The mobile app development world has moved from fast to ‘impatiently fast’. One essential aspect of faster user interaction is the ability to navigate to external websites or open other apps directly from within your Flutter application.

This is where the url_launcher plugin for Flutter comes into play. This plugin allows you to open URLs in the default web browser of the device. It also allows for ​​opening URLs that launch other apps installed on the device such as emails or social media apps. 

Installing URL Launcher in Flutter

Installation can be done in a whiff by following the code given below: 

Terminal Command

flutter pub add url_launcher

This will add a line like this to your package's pubspec.yaml (and run an implicit flutter pub get):

dependencies:
url_launcher: x.y.z.

Supported URL Schemes

url_launcher supports various URL schemes. They are essentially prefixes or protocols that help define how a URL should be handled by Android, iOS or any operating system or apps in general. Common URL Schemes supported by url_launcher include HTTP, HTTPS, mailto, SMS, tel, App Schemes and Custom Schemes. 

Integrating url_launcher

When using the url_launcher package, you can open URLs with these schemes using the launch function. This package will delegate the URL handling to the underlying platform, ensuring compatibility with both Android and iOS. 

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

class MyApp extends StatelessWidget {

@override Widget build(BuildContext context) {

    return MaterialApp(

      home: Scaffold(

        appBar: AppBar(
          title: Text('URL Launcher Example'),
        ),

        body: Center(
          child: ElevatedButton(

            onPressed: () {
              _openURL("https://appxiom.com/");
            },

            child: Text('Open URL'),

          ),

        ),

      ),

    );

  }

  // Function to open a URL using url_launcher

  void _openURL(String url) async {

    if (await canLaunchUrl(url)) { //Checking if there is any app installed in the device to handle the url.

      await launch(url);

    } else {

      // Handle error

    }

  }

}

Configuring canLaunchUrl in iOS

Make sure to add the URL schemes passed to canLaunchUrl as LSApplicationQueriesSchemes entries in your info.plist file. Otherwise, it will return false.

<key>LSApplicationQueriesSchemes</key>

<array>

  <string>sms</string>

  <string>tel</string>

</array>

Configuring canLaunchUrl in Android

Add the URL schemes passed to canLaunchUrl as <queries> entries in your AndroidManifest.xml, otherwise, it will return false in most cases starting on Android 11 (API 30) or higher. 

&lt;!-- Provide required visibility configuration for API level 30 and above --&gt;

&lt;queries&gt;

&nbsp;&nbsp;&lt;!-- If your app checks for SMS support --&gt;

&nbsp;&nbsp;&lt;intent&gt;

&nbsp;&nbsp;&nbsp;&nbsp;&lt;action android:name="android.intent.action.VIEW" /&gt;

&nbsp;&nbsp;&nbsp;&nbsp;&lt;data android:scheme="sms" /&gt;

&nbsp;&nbsp;&lt;/intent&gt;

&nbsp;&nbsp;&lt;!-- If your app checks for call support --&gt;

&nbsp;&nbsp;&lt;intent&gt;

&nbsp;&nbsp;&nbsp;&nbsp;&lt;action android:name="android.intent.action.VIEW" /&gt;

&nbsp;&nbsp;&nbsp;&nbsp;&lt;data android:scheme="tel" /&gt;

&nbsp;&nbsp;&lt;/intent&gt;

&nbsp;&nbsp;&lt;!-- If your application checks for inAppBrowserView launch mode support --&gt;

&nbsp;&nbsp;&lt;intent&gt;

&nbsp;&nbsp;&nbsp;&nbsp;&lt;action android:name="android.support.customtabs.action.CustomTabsService" /&gt;

&nbsp;&nbsp;&lt;/intent&gt;

&lt;/queries&gt;

That’s it for now. For more information on  url_launcher with Flutter, check https://pub.dev/packages/url_launcher/

MANAGING USER EXPERIENCE WHEN BUGS AND PERFORMANCE ISSUES HAPPEN IN ANDROID APP.

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

Even the most meticulously crafted apps may encounter issues or bugs that hinder user satisfaction. To address this challenge, leveraging real-time issue tracking capabilities is essential.

In this blog post, we'll explore how the issue callback listener, the most coveted feature of Appxiom Android SDK can empower developers to proactively improve user experience and manage user retention when performance issues and bugs happen in the app.

Real-Time Issue Tracking with Appxiom Android SDK

The Appxiom Android SDK offers a powerful solution for tracking and reporting issues in real-time within Android apps. One of its standout features is the Issue Callback Listener, which allows developers to receive real-time notifications whenever an issue is detected within their app code-base. By registering a global listener, developers gain access to every issue detected by the SDK.

Get callback when an issue is detected

Integrating the Issue Callback Listener into your Android app is straightforward. Developers can utilize either Java or Kotlin to add the listener (IssueFoundListener) within the Application class.

Java code implementing the IssueFoundListener.

Ax.listenForIssue(new&nbsp;IssueFoundListener()&nbsp;{
@Override
public&nbsp;void&nbsp;issueFoundInBackgroundThread(IssueView&nbsp;issue)&nbsp;{
//Notify the user
}
});
Kotlin code implementing the IssueFoundListener.
Ax.listenForIssue(IssueFoundListener { 
issueView: IssueView? -&gt;
//Notify the user
})

Upon detection of an issue, the callback function is triggered, providing developers with an IssueView object containing datapoints about the issue, including type, severity, occurrence time, and a short description.

Enhance User Experience in Android app by handling issues gracefully

Currently, the developers make use of fields in IssueView class like type and the severity, developers notify the app users (like a toast message) that an issue was detected and will be rectified soon. Read more about adding the listener here.

By promptly identifying and notifying users about issues, developers can enhance user experience, leading to higher user satisfaction and increased app retention rates. Most of the time app users will have no idea if the issues they encounter within the app will be fixed. This feature helps app users to be informed that the issue will be fixed in a future version.

Handle Callbacks in Background Thread

Since the callback function is executed in a background thread, developers should ensure that any UI-related actions are performed on the UI thread to prevent performance issues.

Benefits for Developers

By leveraging the Issue Callback Listener (IssueFoundListener) provided by the Appxiom Android SDK, developers can unlock a multitude of benefits:

  • Proactive Issue Detection: Receive real-time notifications for every issue detected within your app.

  • Comprehensive Issue Information: Gain access to detailed information about each issue, including type, severity, occurrence time, and a short description, facilitating efficient debugging and resolution.

  • Seamless Integration: With a simple integration process, developers can effortlessly incorporate the listener into their Android apps, ensuring minimal disruption to existing workflows.

Visit appxiom.com to learn more about Appxiom.

INTEGRATING COIL IN KOTLIN BASED ANDROID APPS

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

Picture this: You are developing a slick new Kotlin-based Android app. The UI is coming together nicely, but those image thumbnails just won't agree to load as quickly as you would like them to. How to solve this?

Enter Coil, a high-performance library for image loading.

We will jump the gun here to tell you how to go about integrating Coil into your Kotlin-based Apps.

Integrating Coil in Android Kotlin Apps

Integrating Coil onto your Android project using Maven-based dependency management is as easy as 1-2-3. Coil is available by calling the function mavenCentral(). Add the Coil Dependency to your build.gradle file. Open your module-level build.gradle and add the Coil dependency.

dependencies {
implementation(“io.coil-kt:coil:x.y.z”)&nbsp; // Use the latest version
}

Jetpack Compose on Coil

Using Jetpack Compose for building your UI and integrating with Coil for image loading comes with its advantages. This modern Android UI toolkit is designed to simplify and accelerate UI development on Android. Simply, Import the Jetpack Compose extension library and use the following code: 

implementation("io.coil-kt:coil-compose:x.y.z")

And later to  use the AsyncImage composable which comes as a part of coil-compose, to load an image, use the:

AsyncImage(
model = "https://example.com/image.jpg",
contentDescription = null
)

Why use Coil in Android Apps?

Now that we have spoken in detail and how easily you can integrate Coil, let’s also understand why you as an Android App Developer should use Coil. We will fast forward to the functionalities of Coil and how features like memory and disk caching alongside customisations help achieve minimal boilerplate. 

  • Fast Image Loading: Coil helps avoid the complexities of handling image loading manually by focusing on efficiently loading and caching images from various sources, such as URLs, local files etc. This simplistic feature avoids verbose code or any complex configurations. 
// Example of loading an image with Coil
val imageView: ImageView = findViewById(R.id.imageView)
val imageUrl = "https://example.com/image.jpg"
imageView.load(imageUrl)
  • Built-in Transformation Support: Coil allows developers to apply various modifications to images, such as resizing, cropping, or applying filters. This reduces the need for additional code when manipulating images. 
// Example with image transformations
imageView.load(imageUrl) {
transformations(CircleCropTransformation())
}
  • Disk Caching: Caching reduces the need to repeatedly download or load images, enhancing the overall responsiveness of the app.

  • Automatic Request Management: Coil handles the retrieval, decoding, and displaying of the image without requiring extensive manual intervention.

  • ImageView Integration: With Coil, you can easily load an image into an ImageView using Coil's API, making it straightforward to display images in UI components.

// Example of loading an image with Coil
val imageView: ImageView = findViewById(R.id.imageView)
val imageUrl = "https://example.com/image.jpg"
imageView.load(imageUrl)
  • Customisation & Configuration: Developers can configure options such as placeholder images, error images, and image transformations to meet specific requirements.
// Example with placeholder and error handling
imageView.load(imageUrl) {
placeholder(R.drawable.placeholder)
error(R.drawable.error)
}
  • Small Library Size: Coil is designed to be lightweight, making it beneficial for projects where minimising the app's size is a priority. It also makes use of modern libraries including Coroutines, OkHttp, Okio, and AndroidX Lifecycles.

  • Kotlin-Friendly API: Coil is written in Kotlin and offers a Kotlin-friendly API, making it particularly well-suited for projects developed in Kotlin.

For more info and use cases that would make your life easier as a developer, do check out this link. This has an entire repository on how to seamlessly integrate Coil into your Apps. Happy Coding!

GUIDE ON OPTIMIZING FLUTTER APP USING DART ANALYZER

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

Flutter projects come equipped with a dart analyzer for static code analysis. When a project is created with Flutter version 2.3.0 and above, a default configuration file named analysis_options.yaml will be generated at the root of the project. Analyzer tool runs checks based on the checks set in this configuration file.

Lints are a set of rules to check the code for potential errors or formatting issues. The configuration file analysis_options.yaml has a set of recommended lints for Flutter applications, packages, and plugins. This is achieved through the automatic inclusion of package:flutter_lints/flutter.yaml.

include: package:flutter_lints/flutter.yaml

linter:
rules:

Dart-enabled Integrated Development Environments (IDEs) like visual studio code typically display the issues detected by the analyzer in their user interface. Alternatively, you can manually run the analyzer by executing flutter analyze from the terminal to identify and address code issues.

In this blog post, we'll delve into how to customize the lint rules, empowering developers to tailor it to their specific needs.

Customizing Lint Rules in Flutter

The real power of the Dart analyzer configuration lies in the ability to customize lint rules according to your project's requirements. The linter section allows developers to fine-tune lint rules, either by disabling those inherited from flutter.yaml or by enabling additional rules.

Warning: Linter rules may throw false positives.

Configuring Lint rules at the Project level

The analysis_options.yaml configuration file allows developers to customize lint rules at the project level.

linter:
rules:
avoid_print: false
prefer_single_quotes: true

In this example, the avoid_print rule is disabled by setting it to false, and the prefer_single_quotes rule is enabled by setting it to true. This level of granular control allows developers to enforce or relax specific rules based on their project's coding standards.

Configuring Lint rules at File/code level

In addition to configuring lint rules in the global scope as shown above, developers can suppress lints for specific lines of code or files using comments.

The syntax // ignore: name_of_lint or // ignore_for_file: name_of_lint can be used to silence lint warnings on a case-by-case basis.

// ignore_for_file: name_of_lint
class&nbsp;Class&nbsp;{
// ignote: name_of_lint
var&nbsp;_count = 0;

var&nbsp;_count2 = 0;
}

Sample Case Studies

Now, let us dive into couple of lint rules to get a better idea of what exactly these rules can do.

Omitting explicit Local variable types

In situations where functions tend to be concise, local variables often have limited scope. Omitting the variable type helps shift the reader's focus toward the variable's name and its initialized value, which are often more crucial aspects.

With explicit types

List&lt;List&lt;FoodItem&gt;&gt; findMatchingMeals(Set&lt;FoodItem&gt; kitchen) {
List&lt;List&lt;FoodItem&gt;&gt; meals = &lt;List&lt;FoodItem&gt;&gt;[];
for (final List&lt;FoodItem&gt; mealRecipe in recipeBook) {
if (kitchen.containsAll(mealRecipe)) {
meals.add(mealRecipe);
}
}
return meals;
}

Without explicit types

List&lt;List&lt;FoodItem&gt;&gt; findMatchingMeals(Set&lt;FoodItem&gt; kitchen) {
var meals = &lt;List&lt;FoodItem&gt;&gt;[];
for (final mealRecipe in recipeBook) {
if (kitchen.containsAll(mealRecipe)) {
meals.add(mealRecipe);
}
}
return meals;
}

To warn if explicit type is used in the local variables, use the lint rule omit_local_variable_types,

linter:
rules:
-&nbsp;omit_local_variable_types

Disable avoid_print in lint rules

It is always advisable to avoid incorporating print statements into production code. Instead, you can opt for debugPrint or enclose print statements within a condition checking for kDebugMode.

void&nbsp;processItem(int&nbsp;itemId)&nbsp;{
debugPrint('debug: $x');
...
}

void processItem(int itemId) {
if (kDebugMode) {
print('debug: $x');
}
...
}

By default, print statements are flagged by the analyzer.

With lint rules you can override this, set avoid_print to false as shown below,

linter:
rules:
-&nbsp;omit_local_variable_types
avoid_print: false

Conclusion

Customizing the Dart analyzer is a pivotal step in elevating your Flutter development. Begin by activating recommended lints for Flutter, encouraging good coding practices.

The real power lies in the ability to finely tune lint rules at both project and file levels, granting granular control over code standards. Use comments judiciously to suppress lints where needed.

Lastly, The Dart language provides an extensive list of available lint rules, each documented on the official Dart website https://dart.dev/tools/linter-rules#rules.

LOGGING IN FLUTTER

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

Logging plays a vital role in debugging, monitoring, and analyzing the behavior of your Flutter application. Choosing the right logging method and plugin can significantly improve your development workflow and overall code quality.

This blog post explores different logging options available in Flutter, their advantages and disadvantages for each.

Native Logging Methods

These are the most basic logging methods provided by Flutter and Dart. They offer simple syntax and are easily accessible. However, they lack log levels, filtering options, and other features essential for comprehensive logging.

print('This is a simple log message'); 

debugPrint('This message is only visible in debug mode');

To prevent the potential loss of log lines due to Android's log line discarding mechanism when outputting an excessive amount of logs at once, it is advisable to utilize debugPrint() from Flutter's foundation library. This function acts as a wrapper around the standard print method, implementing throttling to ensure that the log output remains at a level that avoids being discarded by Android's kernel.

dart:developer:

This package provides more advanced logging features than print(). It allows logging messages with more granularity.

import 'dart:convert';
import 'dart:developer';

class User {
// Define your custom object properties and methods here.
}

void main() {
// Create an instance of your custom object.
var user = User();

// Log a message with additional application data using the error parameter.
log(
'This is getting logged',
name: 'some.id',
error: jsonEncode(user),
);
}

The log function is used to log a message. The name parameter is used to specify the logging category, and the error parameter is employed to pass additional application data. In this case, the jsonEncode function is used to encode the custom object as a JSON string before passing it to the error parameter.

This approach allows you to view the JSON-encoded data as a structured object when examining the log entries in tools like DevTools.

Advantages

  • Easy to implement and understand.

  • Built-in with Flutter and Dart.

  • Suitable for simple logging needs.

Disadvantages

  • Lack of filtering options and log levels.

  • Not ideal for complex applications with extensive logging needs.

Third party Logging Package

Logger:

Logger is a popular package that offers a powerful and flexible logging API. It provides various log levels, custom filters, and different output destinations.

var logger = Logger(
filter: null,
printer: PrettyPrinter(),
output: null,
);

logger.i('Starting the application');
logger.w('A warning message', error: error);

Link to logger plugin: https://pub.dev/packages/logger

Advantages:

  • More features and flexibility than native methods.

  • Can be extended to have support with external services and platforms.

  • Customizable output and filtering options.

Disadvantages:

  • May require additional setup and dependencies.

  • Can be more complex to use.

Choosing the Right Logging Method

The best logging method for your project depends on your specific needs and requirements. Here are some factors to consider:

  • Project Size and Complexity: Simple projects with minimal logging needs might benefit from native methods like print() or dart:developer. For larger and more complex applications, consider using a dedicated logging package like Logger.

  • Performance Considerations: For performance-critical applications, lightweight packages should be used.

By understanding the advantages and disadvantages of each option and considering your specific requirements, you can make an informed decision and ensure your application is well-equipped for success.

DEVELOPING ANDROID APPS FOR FOLDABLE DEVICES

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

The advent of foldable devices has ushered in a new era of innovation in app development. With screens that seamlessly transition between large and small sizes, developers have a unique canvas to create immersive and adaptive user experiences.

In this blog post, we will explore the key considerations and techniques for developing Android apps optimized for foldable devices.

Responsive/Adaptive Design

Responsive design is the cornerstone of developing apps for foldable devices. The ability of these devices to fold inward or outward poses a challenge that can be met with a responsive layout. Whether you're using ConstraintLayout for traditional views or BoxWithConstraints for Jetpack Compose, the goal is to ensure that your app looks and functions seamlessly across various foldable form factors.

One crucial aspect is avoiding reliance on physical, hardware values for layout decisions. Instead, base your decisions on the actual portion of the screen allocated to your app. This approach guarantees flexibility, making your app adapt well to foldable scenarios like multi-window mode.

The use of WindowManager in a Compose app can provide insights into the current window metrics, allowing your app to make informed decisions based on the available screen space. Converting raw sizes into meaningful size classes, as outlined in the WindowSize Class documentation, further enhances your app's adaptability.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.core.view.WindowCompat
import androidx.window.layout.WindowInfo
import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository

class LoadingActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Optimize for foldables by enabling window features
WindowCompat.setDecorFitsSystemWindows(window, false)

setContent {
val windowSizeClass = remember {
calculateWindowSizeClass(this)
}
ChatApp(windowSizeClass)
}
}
}

@Composable
fun ChatApp(windowSizeClass: WindowSizeClass) {
// Determine whether to show the top app bar based on size class
val showTopAppBar = windowSizeClass.heightSizeClass != WindowHeightSizeClass.Compact

// MyScreen operates independently of window sizes and utilizes a Boolean flag
ChatScreen(
showTopAppBar = showTopAppBar,
)
}
// Function to calculate window size class using WindowInfo
@Composable
fun calculateWindowSizeClass(activity: ComponentActivity): WindowSizeClass {
val windowInfoRepository = windowInfoRepository(activity)
val windowInfo = windowInfoRepository.getSnapshot().getOrDefault(WindowInfo.EMPTY)

return windowInfo.calculateWindowSizeClass()
}

// Additional Composable for displaying the screen content
@Composable
fun ChatScreen(showTopAppBar: Boolean) {
}

Foldable States and Postures

Understanding foldable states and postures is essential for crafting a seamless user experience. Foldable devices can be in various states, such as FLAT or HALF_OPENED, each offering unique layout possibilities. In the HALF_OPENED state, postures like tabletop and book postures introduce further creative opportunities but also pose challenges.

Developers need to ensure that UI elements remain accessible in all device states. Dialog boxes, pop-up menus, and other controls should be positioned strategically to avoid interference with the fold. Accommodating the limitations imposed by the HALF_OPENED state, such as obscured content near the fold, is crucial for delivering a user-friendly design.

App Continuity

App continuity is a key consideration when developing for foldable devices. As these devices fold and unfold, apps may stop and restart, necessitating the seamless restoration of user states.

From retaining typed text in input fields to restoring keyboard states and scroll positions, maintaining continuity enhances the user experience.

Furthermore, app layouts on folded and unfolded screens should complement each other. Users should be able to experience a natural flow between different screen layouts, with content seamlessly transitioning and enhancing the overall user journey.

Drag and Drop Interactions

Foldable devices with large screens provide an ideal canvas for drag and drop interactions. Multi-window mode on foldables allows users to drag and drop content between apps, creating a productive and engaging experience. Developers can leverage the Android drag and drop framework to implement these interactions, adding a layer of sophistication to the apps.

Why Develop Apps for Foldable Devices?

  • Innovative User Experiences: Foldable devices offer a unique canvas for creative and innovative user experiences. By embracing the flexibility of foldable screens, developers can design apps that stand out in the crowded app landscape.

  • Expanded Market Reach: As foldable devices become more popular, developing apps optimized for these devices opens up new opportunities and expands your app's potential user base. Catering to emerging trends ensures that your app remains relevant in a rapidly evolving tech landscape.

  • Differentiation in a Competitive Market: Developing for foldable devices allows you to differentiate your app from competitors. Users are often drawn to apps that leverage the full potential of their devices, and being an early adopter of foldable technology can set your app apart.

As the market for foldable devices continues to grow, developers who adapt to this evolving landscape position themselves at the forefront will benefit.

UNDERSTANDING FLOW IN KOTLIN

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

Kotlin, a statically-typed, modern programming language, has introduced a powerful asynchronous programming concept called Flow. Flow simplifies asynchronous programming in Kotlin, making it more efficient and expressive.

In this blog post, we'll delve into what Flow is, how to use it, and provide practical examples in the context of Android app development. We'll explore how to send internal notifications to different modules within an Android application using Kotlin Flow.

What is Flow in Kotlin?

Flow is a new asynchronous stream processing library introduced in Kotlin, specifically designed to handle streams of data asynchronously and in a non-blocking way. It's built on top of Kotlin's coroutines and allows you to work with sequences of values as they become available.

Key features of Flow:

  • Asynchronous: Flow is designed for asynchronous operations and is perfect for use cases where you want to handle data asynchronously without blocking the main thread.

  • Non-blocking: Flow works seamlessly with Kotlin's coroutines, which means it is non-blocking and doesn't freeze your app while processing data.

  • Backpressure: Flow can handle backpressure, allowing you to control the flow of data between the producer and consumer, preventing overloading of resources.

  • Composability: Flow can be easily combined, transformed, and modified, making it a versatile tool for stream processing.

How to use Flow in Kotlin?

To use Flow in your Kotlin application, follow these steps:

1. Import the necessary dependencies:

To use Flow in an Android app, you need to include the Kotlin coroutines library in your project.

You can add it to your app-level build.gradle file:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" // Use the latest version

2. Create a Flow:

You can create a Flow using the following code, providing the values you want to emit.

For example:

fun fetchUserData(): Flow&lt;User&gt; = flow {
// Fetch user data asynchronously
val user = api.fetchUser()
emit(user)
}

3. Collect data from a Flow:

To consume data from a Flow, you can use the collect extension function. This allows you to receive emitted values and handle them:

viewModelScope.launch {
fetchUserData().collect { user -&gt;
// Handle the user data
}
}

4. Transform and combine Flows:

You can use various operators on Flow to transform, filter, and combine multiple Flows. Some common operators include map, filter, zip, and merge.

val transformedFlow = fetchUserData()
.map { user -&gt; user.name }

Now that you understand the basics of using Flow, let's explore a practical example in an Android app.

Sending Internal Notifications in an Android App

In Android app development, you often need to communicate between different modules or components within your app. Using Flow, you can send internal notifications from one module to another without tightly coupling them. Here's how you can achieve this,

Create a Notification Flow:

Define a Flow that represents the notifications you want to send. For example, let's create a simple Flow for sending notifications of new messages:

object MessageNotificationManager {
private val notificationFlow = MutableSharedFlow&lt;Message&gt;()

fun sendNotification(message: Message) {
viewModelScope.launch {
notificationFlow.emit(message)
}
}

fun getNotificationFlow(): Flow&lt;Message&gt; = notificationFlow
}

In this example, MessageNotificationManager provides methods to send notifications and get the Flow to receive notifications.

Subscribe to the Notification Flow:

In the module that needs to receive notifications, subscribe to the Flow.

class ChatFragment : Fragment() {
// ...

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

MessageNotificationManager.getNotificationFlow()
.onEach { message -&gt;
// Handle the new message notification
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
}

In this example, ChatFragment listens to the notification Flow using onEach and processes incoming messages.

Sending Notifications:

In the module where you want to send notifications, call the sendNotification method.

val newMessage = Message("Hello, world!")
MessageNotificationManager.sendNotification(newMessage)

By using Flow to send and receive internal notifications, you decouple different parts of your Android app, making it more modular and maintainable.

Use Cases for Flow in Android Apps

Flow is a powerful tool for handling asynchronous and non-blocking operations in Android apps. Here are some common use cases for Flow:

  • Data fetching and processing: Use Flow to fetch data from network requests, databases, or other sources asynchronously.

  • Real-time updates: Implement real-time features in your app, such as chat applications or live notifications, using Flow to handle data updates.

  • User interface updates: Update UI components when data changes, ensuring a smooth and responsive user experience.

  • Event handling: Manage and process events, such as button clicks, gestures, or sensor data, using Flow to handle events as they occur.

Conclusion

In Android app development, Flow allows for efficient communication between different modules and components, making your code more modular and maintainable. By following the steps outlined in this blog post and considering the practical examples, you can effectively incorporate Flow into your Kotlin-based Android applications.

IMPLEMENTING AND USING DATA STRUCTURES IN DART

Published: · Last updated: · 5 min read
Robin Alex Panicker
Cofounder and CPO, Appxiom

Dart is a versatile and powerful programming language that has gained popularity for building web, mobile, and desktop applications, thanks to the Flutter framework. To harness its full potential, it's essential to understand and implement various data structures.

In this blog, we'll explore some common data structures and demonstrate how to implement and use them in Dart.

Data Structures in Dart

Data structures are fundamental for organizing and managing data efficiently. Dart provides built-in support for a variety of data structures and allows you to create custom ones. Some common data structures in Dart include:

  • Lists

  • Sets

  • Maps

  • Queues

  • Stacks

  • Trees

  • Graphs

We'll delve into each of these data structures, provide code samples, and discuss their use cases.

Lists

Lists in Dart are ordered collections of objects. They are similar to arrays in other languages and are incredibly versatile. Here's how to create and manipulate lists:

// Creating a List
List&lt;int&gt; numbers = [1, 2, 3, 4, 5];

// Accessing elements
int firstNumber = numbers[0]; // Access the first element (1)

// Modifying elements
numbers[2] = 10; // Update the third element to 10

// Adding elements
numbers.add(6); // Add 6 to the end of the list

// Removing elements
numbers.remove(2); // Remove the element with value 2

// Iterating through a list
for (var number in numbers) {
print(number);
}

Sets

Sets are unordered collections of unique elements. Dart's Set ensures that each element is unique, making them suitable for maintaining unique values:

// Creating a Set
Set&lt;String&gt; uniqueColors = {'red', 'blue', 'green'};

// Adding elements
uniqueColors.add('yellow');

// Removing elements
uniqueColors.remove('red');

// Iterating through a set
for (var color in uniqueColors) {
print(color);
}

Maps

Maps, also known as dictionaries, are collections of key-value pairs. In Dart, maps are implemented using the Map class:

// Creating a Map
Map&lt;String, int&gt; ages = {'Alice': 25, 'Bob': 30, 'Charlie': 22};

// Accessing values
int aliceAge = ages['Alice']; // Access Alice's age (25)

// Modifying values
ages['Bob'] = 31; // Update Bob's age to 31

// Adding new key-value pairs
ages['David'] = 28; // Add a new entry

// Removing key-value pairs
ages.remove('Charlie'); // Remove Charlie's entry

// Iterating through a map
ages.forEach((name, age) {
print('$name is $age years old');
});

Queues

A queue is a data structure that follows the First-In-First-Out (FIFO) principle. In Dart, you can create a simple Queue data structure using a custom class:

class Queue&lt;T&gt; {
List&lt;T&gt; _items = [];

void enqueue(T item) {
_items.add(item);
}

T dequeue() {
if (_items.isNotEmpty) {
return _items.removeAt(0);
}
return null;
}

int get length =&gt; _items.length;
}

You can then use this custom Queue class as follows:

var myQueue = Queue&lt;int&gt;();
myQueue.enqueue(1);
myQueue.enqueue(2);
myQueue.enqueue(3);

print(myQueue.dequeue()); // 1

Queues are useful for tasks that require managing elements in the order they were added, such as task scheduling or breadth-first search in graphs.

Stacks

A stack is another fundamental data structure that follows the Last-In-First-Out (LIFO) principle. While Dart doesn't provide a built-in Stack class, you can easily implement one using a custom class:

class Stack&lt;T&gt; {
List&lt;T&gt; _items = [];

void push(T item) {
_items.add(item);
}

T pop() {
if (_items.isNotEmpty) {
return _items.removeLast();
}
return null;
}

int get length =&gt; _items.length;
}

You can use this custom Stack class as follows:

var myStack = Stack&lt;int&gt;();
myStack.push(1);
myStack.push(2);
myStack.push(3);

print(myStack.pop()); // 3

Stacks are often used for tasks like managing function calls, parsing expressions, and implementing undo/redo functionality in applications.

Trees

Trees are hierarchical data structures with nodes connected by edges. They are commonly used for organizing data, searching, and representing hierarchical relationships. In Dart, you can create tree-like structures by defining custom classes that represent nodes. Here's a basic example of a binary tree:

class TreeNode&lt;T&gt; {
T value;
TreeNode&lt;T&gt; left;
TreeNode&lt;T&gt; right;

TreeNode(this.value);
}

You can then build a tree structure by connecting these nodes. Tree data structures come in various forms, including binary trees, AVL trees, and B-trees, each suited for specific tasks.

Graphs

Graphs are complex data structures that consist of nodes and edges. They are used to represent relationships between objects and solve problems such as network routing, social network analysis, and more. In Dart, you can create a basic graph using a custom class to represent nodes and edges:

class Graph&lt;T&gt; {
Map&lt;T, List&lt;T&gt;&gt; _adjacencyList = {};

void addNode(T node) {
if (!_adjacencyList.containsKey(node)) {
_adjacencyList[node] = [];
}
}

void addEdge(T node1, T node2) {
_adjacencyList[node1].add(node2);
_adjacencyList[node2].add(node1); // For an undirected graph
}

List&lt;T&gt; getNeighbors(T node) {
return _adjacencyList[node];
}
}

void main() {
var graph = Graph&lt;String&gt;();

graph.addNode('A');
graph.addNode('B');
graph.addNode('C');
graph.addNode('D');

graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('B', 'D');

print(graph.getNeighbors('A')); // [B, C]
print(graph.getNeighbors('B')); // [A, D]
}

This is a basic implementation of an undirected graph in Dart. You can expand upon this to create more complex graphs and perform various operations.

Conclusion

Understanding and implementing data structures in Dart is essential for efficient and organized data manipulation in your programs.

Lists, Sets, and Maps are the built-in data structures that come in handy for most scenarios, but you can create custom data structures like Queues and Stacks when necessary. Trees and Graphs are more complex data structures that can be implemented through custom classes to solve specific problems.

With this knowledge, you'll be better equipped to tackle a wide range of programming challenges in Dart.

GET STARTED WITH GLIDE KOTLIN LIBRARY FOR ANDROID

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

Glide is an image loading library for Android that is known for its speed and efficiency. It is an open-source library that is maintained by Google and is used by many popular apps, such as Twitter, Facebook, and Amazon.

Advantages of using Glide

There are many reasons why you might want to use Glide in your Android app. Here are a few:

  • Performance: Glide is one of the fastest image loading libraries available for Android. It is able to load images quickly and efficiently, even on large or complex images.

  • Memory efficiency: Glide is also very memory-efficient. It uses a variety of techniques to reduce the memory usage of your app, such as image caching and image recycling.

  • Flexibility: Glide is very flexible and can be used to load images from a variety of sources, including local storage, network resources, and even other image loading libraries.

  • Ease of use: Glide is easy to use and integrate into your Android app. It provides a simple API that makes it easy to get started with loading images.

Integrating Glide with an Android Kotlin project

To integrate Glide with your Android Kotlin project, you will need to follow these steps:

  • Add the Glide dependency to your project's build.gradle file.
// Add the Glide dependency to your project's build.gradle file.
dependencies {
implementation 'com.github.bumptech.glide:glide:x.x.x'
}
  • Create a Glide object in your activity or fragment.
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions


Glide.with(this)
.load(R.drawable.your_image) // Replace with your image resource or URL
.into(imageView)
  • Use the Glide object to load images into your app.
// Use the Glide object to load images into your app.
Glide.with(this).load("https://example.com/image.png").into(imageView)

Top features of Glide

Glide provides a variety of features that make it a powerful and versatile image loading library. Here are a few of the most notable features:

  • Image caching: Glide caches images in memory and on disk to improve performance and reduce network traffic.

  • Image decoding: Glide can decode images from a variety of formats, including JPEG, PNG, GIF, and WebP.

  • Image transformations: Glide can perform a variety of image transformations, such as cropping, scaling, and rotating.

Glide.with(this)
.load(R.drawable.your_image)
.apply(RequestOptions.circleCropTransform()) // Circular transformation
.into(imageView)
  • Image placeholders: Glide can display placeholder images while images are loading.
Glide.with(this)
.load(R.drawable.your_image)
.placeholder(R.drawable.placeholder_image)
.into(imageView)
  • Error handling: Glide provides robust error handling to ensure that images are always displayed correctly, even if there is an error loading the image.
Glide.with(this)
.load(R.drawable.your_image)
.error(R.drawable.error_image)
.into(imageView)

Glide is a powerful and versatile image loading library for Android applications. It simplifies the process of loading and displaying images, offers extensive customization options, and handles caching efficiently. By following the steps outlined in this blog, you can quickly integrate Glide into your Kotlin Android app and provide a smoother image loading experience for your users.

COMPLYING WITH GDPR IN IOS APPS: A COMPREHENSIVE GUIDE

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

The General Data Protection Regulation (GDPR) is a European Union regulation that aims to protect the privacy and personal data of individuals. If your iOS app collects, processes, or stores personal data of EU residents, you must ensure that your app complies with GDPR.

In this guide, we will walk you through the steps to comply with GDPR in your iOS app, complete with Swift code samples.

1. Understand GDPR Principles

Before we dive into the technical aspects, it's crucial to understand the key principles of GDPR:

  • Consent: Users must give informed, explicit consent for their data to be collected and processed.

  • Data Minimization: Only collect and process data that is necessary for your app's functionality.

  • Data Portability: Users have the right to access and transfer their data.

  • Data Security: Implement robust security measures to protect user data.

  • Data Deletion: Allow users to delete their data upon request.

2. Inform Users with a Privacy Policy

Start by providing a clear and concise privacy policy within your app. You can display this during the onboarding process or in the app settings. Ensure that users can easily access and understand your privacy policy.

// Load and display the privacy policy HTML content
if let privacyPolicyURL = URL(string: "https://example.com/privacy-policy.html") {
let request = URLRequest(url: privacyPolicyURL)
webView.load(request)
}

To collect and process user data, you must obtain explicit consent. Create a consent dialog that explains why you need their data and how you will use it. Provide options for users to accept or reject data collection.

// Display a consent dialog
let alertController = UIAlertController(
title: "Data Collection Consent",
message: "We need your data to provide personalized recommendations. Do you consent?",
preferredStyle: .alert
)

let consentAction = UIAlertAction(title: "Consent", style: .default) { _ in
// User consented; start data collection
}

let rejectAction = UIAlertAction(title: "Reject", style: .destructive) { _ in
// User rejected data collection; handle accordingly
}

alertController.addAction(consentAction)
alertController.addAction(rejectAction)
present(alertController, animated: true, completion: nil)

4. Implement Data Minimization

Collect only the data necessary for your app's functionality. Avoid unnecessary data collection to minimize the risk of GDPR violations. Remove any unused data promptly.

5. Secure Data Storage

Protect user data by securely storing it. Use Apple's Keychain Services for sensitive data like passwords and tokens:

import Security

// Store a user's access token securely in the keychain
let accessToken = "user_access_token"
let accessTokenData = accessToken.data(using: .utf8)!
let keychainQuery = [
kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String: "userAccessToken",
kSecValueData as String: accessTokenData
] as CFDictionary
let status = SecItemAdd(keychainQuery, nil)
if status == errSecSuccess {
print("Access token stored securely.")
}

6. Enable Data Portability

Implement a feature that allows users to access and export their data. Provide options to download data in common formats like JSON or CSV.

// Allow users to export their data
func exportUserData() {
// Prepare user data for export
let userData = ["name": "John Doe", "email": "johndoe@example.com"]

// Convert data to JSON
if let jsonData = try? JSONSerialization.data(withJSONObject: userData, options: []) {
// Offer the JSON file for download
let activityViewController = UIActivityViewController(activityItems: [jsonData], applicationActivities: nil)
present(activityViewController, animated: true, completion: nil)
}
}

7. Implement Data Deletion

Users have the right to request the deletion of their data. Create a feature to allow users to delete their accounts and associated data.

// Delete user account and data
func deleteUserAccount() {
// Perform data deletion// ...// Notify the user
let alertController = UIAlertController(
title: "Account Deleted",
message: "Your account and data have been deleted.",
preferredStyle: .alert
)
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
// Handle user acknowledgment
}
alertController.addAction(okAction)
present(alertController, animated: true, completion: nil)
}

8. Handle Data Breaches

In the unfortunate event of a data breach, you must notify both users and authorities as required by GDPR. Implement a mechanism to detect and respond to breaches promptly.

9. Regularly Update and Audit

Stay compliant by regularly updating your app and privacy policy to reflect changes in data handling practices and GDPR regulations. Perform periodic data audits to ensure compliance.

Conclusion

Complying with GDPR in your iOS app is crucial to protect user privacy and avoid legal consequences. By following the principles of consent, data minimization, security, and providing user rights, you can build a GDPR-compliant app. Always stay informed about evolving GDPR regulations and adapt your app accordingly to maintain compliance.

DIO PLUGIN INTEGRATION WITH DART / FLUTTER: FOR BEGINNERS

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

Dio is a popular HTTP client library for Dart and Flutter. It provides a comprehensive and high-performance API for making HTTP requests, with support for multiple core features.

Why use Dio in Flutter?

Dio offers a number of advantages over the built-in http package in Flutter, including:

  • More features: Dio provides a wider range of features than the http package, such as global configuration, interceptors, and request cancellation.

  • Better performance: Dio is generally considered to be more performant than the http package, especially for complex requests.

  • Easier to use: Dio provides an intuitive and easy-to-use API, making it a good choice for both beginners and experienced developers.

How to integrate Dio with a Flutter project

To integrate Dio with a Flutter project, you can follow these steps:

  • Add the Dio dependency to your pubspec.yaml file:
dependencies:
dio: ^5.3.3
  • Run flutter pub get to install the Dio package.

  • Create a new Dio instance:

import 'package:dio/dio.dart';

class MyApiClient {
final dio = Dio();
}
  • Make HTTP requests using the Dio instance:
Future&lt;Response&gt; get(String url) async {
return await dio.get(url);
}

Future&lt;Response&gt; post(String url, dynamic data) async {
return await dio.post(url, data: data);
}
  • Handle errors:
try {
Response response = await dio.get(url);

// Handle the response
} catch (e) {
// Handle the error
}

Features of the Dio plugin

Dio provides a number of features that make it a powerful and versatile HTTP client for Flutter, including:

  • Global configuration:Dio allows you to set global configurations that apply to all requests made by the client. This includes options like setting default headers, base URLs, and more.

  • Interceptors: Dio supports interceptors, which allow you to intercept and modify requests and responses. This can be used to implement features such as authentication, logging, and caching.

  • Request cancellation: Dio allows you to cancel requests in progress. This can be useful if you need to stop a request that is no longer needed.

  • File downloading: Dio provides a built-in file downloader that can be used to download files from the server.

  • Timeout: Dio allows you to set a timeout for requests. This can be useful to prevent requests from hanging indefinitely.

Disadvantages of using Dio over http in Flutter

Dio has a few potential disadvantages over the built-in http package in Flutter, including:

  • Larger package size: The Dio package is larger than the http package, which can increase the size of your Flutter app.

  • Steeper learning curve: Dio provides more features than the http package, which can make it more difficult to learn.

  • Community support: The http package is more widely used than Dio, so there is a larger community of developers who can provide support.

Overall, Dio is a powerful and versatile HTTP client for Flutter that offers a number of advantages over the built-in http package.

COROUTINEWORKER: THE KOTLIN-FRIENDLY WAY TO RUN ASYNCHRONOUS TASKS IN ANDROID

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

CoroutineWorker is a Kotlin-specific class in Android WorkManager that allows you to run asynchronous tasks in the background. It is a wrapper around the Worker class, and it provides a number of benefits, including:

  • Support for Kotlin Coroutines: CoroutineWorker uses Kotlin Coroutines to run asynchronous tasks, which makes your code more concise and expressive.

  • Automatic handling of stoppages and cancellation: CoroutineWorker automatically handles stoppages and cancellation, so you don't have to worry about manually managing these states.

  • Better performance: CoroutineWorker can often perform better than the Worker class because it uses Kotlin Coroutines to manage its state.

  • To take advantage of the latest Android features: CoroutineWorker is a newer class than the Worker class, and it supports some of the latest Android features, such as Kotlin Flow.

Implementing CoroutineWorker

To use CoroutineWorker, you first need to create a subclass of the CoroutineWorker class. In your subclass, you need to implement the doWork() method. This method is where you will write your asynchronous code.

Here is a simple example of a CoroutineWorker subclass:

class MyCoroutineWorker : CoroutineWorker() {

override suspend fun doWork(): Result {
// Perform your asynchronous task here.

return Result.success()
}
}

Once you have created your CoroutineWorker subclass, you can schedule it to run using the WorkManager class.

Here is an example of how to schedule a CoroutineWorker to run:

val work = OneTimeWorkRequestBuilder&lt;MyCoroutineWorker&gt;()
.build()

WorkManager.getInstance(context).enqueue(work)

Scheduling Tasks with CoroutineWorker

You can do this by using the PeriodicWorkRequestBuilder class. This class allows you to create a WorkRequest that will be executed periodically at a specified interval.

Here is an example of how to schedule a task to run periodically with CoroutineWorker:

val work = PeriodicWorkRequestBuilder&lt;MyCoroutineWorker&gt;(
repeatInterval = Duration.ofHours(1)
).build()

WorkManager.getInstance(context).enqueue(work)

This code will schedule a CoroutineWorker to run every hour. The CoroutineWorker will be executed in the background, and it will be able to access all of the same resources as a regular CoroutineWorker.

You can also schedule tasks with CoroutineWorker to run only once. To do this, you can use the OneTimeWorkRequestBuilder class. This class allows you to create a WorkRequest that will be executed once and then discarded.

When to use CoroutineWorker

You should use CoroutineWorker whenever you need to run an asynchronous task in the background. This includes tasks such as:

  • Downloading data from the internet

  • Uploading data to the cloud

  • Performing database queries

  • Processing images

  • Sending notifications

CoroutineWorker is a good choice for these types of tasks because it is easy to use, efficient, and reliable.

Limitations of CoroutineWorker

Here are some cases where you should not use CoroutineWorker:

  • If you need to run a task that requires a lot of CPU or memory resources. CoroutineWorker is designed for running lightweight background tasks. If you need to run a task that requires a lot of resources, you should use a different approach, such as starting a service or using a foreground process.

  • If you need to run a task that needs to be able to interact with the UI. CoroutineWorker runs in the background, so it cannot interact with the UI directly. If you need to run a task that needs to be able to interact with the UI, you should use a different approach, such as using a worker thread or posting a message to the main thread.

  • If you need to run a task that is critical to the user experience. CoroutineWorker does not guarantee that tasks will be executed in a timely manner. If you need to run a task that is critical to the user experience, you should use a different approach, such as using a service or using a foreground process.

Overall, CoroutineWorker is a good choice for running lightweight background tasks in Android apps. However, it is important to be aware of its limitations and to use it in the appropriate scenarios.

SOLVING FRAME RATE ISSUES AND APP HANGS IN SWIFTUI IOS APPS

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

Developing a smooth and responsive iOS app is crucial for providing a great user experience. Frame rate issues and app hangs can be frustrating for users and can lead to negative reviews and decreased app usage.

In this blog post, we will explore common causes of frame rate issues and app hangs in SwiftUI iOS apps and provide solutions and code examples to address them.

Understanding Frame Rate Issues

Frame rate issues occur when an app struggles to render frames at the desired rate, usually 60 frames per second (FPS) on most iOS devices. When the frame rate drops, animations become choppy, and the app feels less responsive. There are several common reasons for frame rate issues:

  • Inefficient View Updates: SwiftUI's declarative nature encourages frequent view updates. If not optimized, this can lead to excessive rendering and reduced frame rates.

  • Heavy Computation on the Main Thread: Performing CPU-intensive tasks on the main thread can block the UI, making the app feel unresponsive.

  • Large Images and Assets: Loading or rendering large images or assets can consume significant memory and processing power, leading to frame rate drops.

Solving Frame Rate Issues in SwiftUI

1. Optimize View Updates

You can optimize view updates by:

  • Using the .onAppear and .onDisappear modifiers to load data only when necessary.

  • Implementing the .id modifier to identify views uniquely and avoid unnecessary updates.

  • Reducing the complexity of SwiftUI view hierarchies.

Example:

struct ContentView: View {
var body: some View {
Text("Optimize your views")
.onAppear {
// Load data when the view appears
loadData()
}
}
}

2. Offload Heavy Computation

Move CPU-intensive tasks to background threads using DispatchQueue or Combine. Ensure that UI updates occur on the main thread.

Using DispatchQueue:

DispatchQueue.global().async {
// Perform heavy computation
let result = performHeavyComputation()

DispatchQueue.main.async {
// Update the UI on the main thread
self.resultLabel = result
}
}

Combine is a powerful framework for handling asynchronous and event-driven code in Swift. You can use Combine to perform background operations in SwiftUI seamlessly. In this example, we'll demonstrate how to use Combine to execute a background operation and update the SwiftUI view when the operation completes.

Let's say you want to fetch some data from a network API in the background and update your SwiftUI view when the data is ready. Here's a step-by-step guide:

  1. Import Combine in your SwiftUI view file:
import SwiftUI
import Combine
  1. Define a ViewModel to handle your data and background operations. Create an ObservableObject class that will hold your data and expose a publisher for notifying view updates.
class MyViewModel: ObservableObject {
@Published var data: [YourDataType] = [] // Replace YourDataType with the actual data type you're using
private var cancellables: Set&lt;AnyCancellable&gt; = []

func fetchData() {
// Simulate a background network request
fetchDataFromNetwork()
.receive(on: DispatchQueue.main) // Ensure updates are on the main thread
.sink { completion in
// Handle completion or errors if needed
} receiveValue: { [weak self] newData in
self?.data = newData
// Update the data when received
}
.store(in: &amp;cancellables)
}

private func fetchDataFromNetwork() -&gt; AnyPublisher&lt;[YourDataType], Error&gt; {
// Implement your network request logic here and return a Combine publisher
// For example, you can use URLSession's dataTaskPublisher
let url = URL(string: "https://your-api-url.com/data")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [YourDataType].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}

Replace YourDataType with the actual type of data you're fetching from the network.

  1. Create a SwiftUI view that observes the changes in your ViewModel and triggers the background operation:
struct ContentView: View {
@ObservedObject private var viewModel = MyViewModel()

var body: some View {
VStack {
if viewModel.data.isEmpty {
Text("Loading...")
} else {
List(viewModel.data, id: \.self) { item in
// Display your data here
Text(item.name)
// Replace with your data properties
}
}
}
.onAppear {
viewModel.fetchData()
// Trigger the background operation when the view appears
}
}
}

In this SwiftUI view, the @ObservedObject property wrapper observes changes to the viewModel, and the onAppear modifier triggers the background operation by calling viewModel.fetchData() when the view appears.

Now, your SwiftUI view will fetch data from the network in the background using Combine and update the view when the data is ready, providing a smooth and responsive user experience.

3. Efficiently Manage Images and Assets

Load images lazily and use asset catalogs for managing image resources. Resize images to appropriate dimensions to reduce memory usage.

In SwiftUI, you can load images lazily using the AsyncImage view. AsyncImage allows you to load and display images asynchronously, which is especially useful for large images or images fetched from the network. Here's how you can use AsyncImage to load images lazily in SwiftUI:

import SwiftUI

struct LazyLoadingImageView: View {
let imageURL: URL
var body: some View {
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
// Placeholder while loading (optional)
ProgressView()
case .success(let image):
// Successfully loaded image
image
.resizable()
.scaledToFit()
case .failure(_):
// Handle the failure (e.g., show an error message)
Image(systemName: "xmark.octagon")
.resizable()
.scaledToFit()
.foregroundColor(.red)
@unknown default:
// Handle other unknown states
Text("Unknown state")
}
}
}
}

In the code above:

  • AsyncImage is used to load the image asynchronously from the specified URL.

  • The closure inside AsyncImage receives a Phase parameter, which represents the current state of the image loading process.

  • In the .empty phase, you can display a placeholder (e.g., a ProgressView) to indicate that the image is being loaded.

  • In the .success phase, you can display the loaded image, making it resizable and scaling it to fit the available space.

  • In the .failure phase, you can handle the failure by displaying an error image or a message.

  • The @unknown default case is used to handle any unknown states that might be introduced in future SwiftUI versions.

To use the LazyLoadingImageView in your SwiftUI view, simply provide the URL of the image you want to load:

struct ContentView: View {
var body: some View {
LazyLoadingImageView(imageURL: URL(string: "https://example.com/image.jpg")!)
.frame(width: 200, height: 200)
}
}

Make sure to replace "https://example.com/image.jpg" with the actual URL of the image you want to load.

With AsyncImage, you can efficiently load and display images in a lazy manner, ensuring a smooth user experience, especially when dealing with large images or images from remote sources.

Addressing App Hangs

App hangs occur when the app becomes unresponsive for 250 milli seconds or more due to various reasons, such as blocking the main thread or network requests taking too long. Here are some strategies to prevent app hangs:

1. Use Background Threads for Network Requests

Perform network requests on background threads to avoid blocking the main thread. Combine or URLSession can be used for this purpose.

Example:

let cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: MyModel.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { _ in }) { data in
// Process data and update UI
}

2. Implement Error Handling

Handle errors gracefully, especially in asynchronous operations, to prevent app hangs and crashes.

do {
let result = try performRiskyOperation()
// Handle the result
} catch {
// Handle the error
}

3. Use Xcode Instruments and APM tools

Use Xcode's Instruments to profile your app's performance, identify bottlenecks, and monitor memory usage. Debugging tools like LLDB can help trace and fix specific issues causing frame rate issues and app hangs.

APM tools are a very much helpful in detecting frame rate issues and App Hangs. Appxiom is such an APM tool that helps in detecting these issues and provides all relevant data points to the developer to fix the issue.

Conclusion

Frame rate issues and app hangs can significantly impact the user experience of your SwiftUI iOS app. By optimizing view updates, offloading heavy computation, efficiently managing assets, and addressing app hangs through proper threading and error handling, you can create a smooth and responsive app that users will love.

Remember that performance optimization is an ongoing process. Regularly test your app on different devices and keep an eye on performance metrics to ensure a consistently great user experience.