Skip to main content

137 posts tagged with "Apps"

View All Tags

BEST PRACTICES TO AVOID MEMORY LEAKS IN FLUTTER

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

Memory leaks can be a common issue in mobile app development, including Flutter applications. When memory leaks occur, they can lead to reduced performance, increased memory consumption, and ultimately, app crashes. Flutter developers must be proactive in identifying and preventing memory leaks to ensure their apps run smoothly.

In this blog post, we will explore some best practices to help you avoid memory leaks in your Flutter applications, complete with code examples.

1. Use Weak References

One of the most common causes of memory leaks in Flutter is holding strong references to objects that are no longer needed. To prevent this, use weak references when appropriate. Weak references allow objects to be garbage collected when they are no longer in use.

Here's an example of how to use weak references in Flutter:

import 'dart:ui';

class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
// Use a weak reference to avoid memory leaks
final _myObject = WeakReference<MyObject>();

@override
void initState() {
super.initState();
// Create an instance of MyObject
_myObject.value = MyObject();
}

@override
Widget build(BuildContext context) {
// Use _myObject.value in your widget
return Text(_myObject.value?.someProperty ?? 'No data');
}
}

2. Dispose of Resources

In Flutter, widgets that use resources such as animations, controllers, or streams should be disposed of when they are no longer needed. Failure to do so can result in memory leaks.

Here's an example of how to dispose of resources using the dispose method:

class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);
}

@override
void dispose() {
_controller.dispose(); // Dispose of the animation controller
super.dispose();
}

@override
Widget build(BuildContext context) {
// Use the _controller for animations
return Container();
}
}

3. Use WidgetsBindingObserver

Flutter provides the WidgetsBindingObserver mixin, which allows you to listen for app lifecycle events and manage resources accordingly. You can use it to release resources when the app goes into the background or is no longer active.

Here's an example of how to use WidgetsBindingObserver:

class MyWidget extends StatefulWidget with WidgetsBindingObserver {
@override
_MyWidgetState createState() => _MyWidgetState();

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
// Release resources when the app goes into the background
_releaseResources();
} else if (state == AppLifecycleState.resumed) {
// Initialize resources when the app is resumed
_initializeResources();
}
}

void _initializeResources() {
// Initialize your resources here
}

void _releaseResources() {
// Release your resources here
}
}

4. Use Flutter DevTools

Flutter DevTools is a powerful set of tools that can help you identify and diagnose memory leaks in your Flutter app. It provides insights into memory usage, object allocation, and more. To use Flutter DevTools, follow these steps:

  • Ensure you have Flutter DevTools installed:
flutter pub global activate devtools
  • Run your app with DevTools:
flutter run
  • Open DevTools in a web browser:
flutter pub global run devtools
  • Use the Memory and Performance tabs to analyze memory usage and detect leaks.

5. Use APM Tools

Even if a thorough testing is done, chances of memory leaks happening in production cannot be ruled out. Use APM tools like Appxiom that monitors memory leaks and reports in real time, both in development phase and production phase.

Conclusion

Memory leaks can be a challenging issue to deal with in Flutter apps, but by following these best practices and using tools like Flutter DevTools and Appxiom, you can significantly reduce the risk of memory leaks and keep your app running smoothly. Remember to use weak references, dispose of resources properly, and manage resources based on app lifecycle events to ensure your Flutter app remains efficient and stable.

Happy Coding!

INTEGRATING AND USING CHARTS IN FLUTTER

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

Data visualization is a crucial aspect of mobile app development. Flutter, a popular open-source framework for building natively compiled applications for mobile, web, and desktop from a single codebase, offers various libraries and tools to integrate and use charts effectively.

In this article, we will explore how to integrate and use charts in Flutter applications.

Let's dive in!

1. Setting Up a Flutter Project

Before we begin, make sure you have Flutter installed on your system. If not, you can follow the official Flutter installation guide: https://flutter.dev/docs/get-started/install

Once Flutter is set up, create a new Flutter project using the following command:

flutter create flutter_chart_example

Navigate to the project directory:

cd flutter_chart_example

Now, you're ready to integrate charts into your Flutter app.

2. Choosing a Charting Library

Flutter offers various charting libraries to choose from, including fl_chart, charts_flutter, and syncfusion_flutter_charts.

In this article, we'll use fl_chart, a versatile and customizable charting library.

3. Installing the Charting Library

Open the pubspec.yaml file in your Flutter project and add the fl_chart dependency:

dependencies:
flutter:
sdk: flutter
fl_chart: ^0.63.0

Run flutter pub get to install the dependency:

flutter pub get

4. Creating a Basic Chart

Let's create a basic line chart to display some sample data. Open the main.dart file and replace its content with the following code:

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

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Chart Example'),
),
body: Center(
child: LineChart(
LineChartData(
gridData: FlGridData(show: false),
titlesData: FlTitlesData(show: false),
borderData: FlBorderData(
show: true,
border: Border.all(
color: const Color(0xff37434d),
width: 1,
),
),
minX: 0,
maxX: 7,
minY: 0,
maxY: 6,
lineBarsData: [
LineChartBarData(
spots: [
FlSpot(0, 3),
FlSpot(1, 1),
FlSpot(2, 4),
FlSpot(3, 2),
FlSpot(4, 5),
FlSpot(5, 3),
FlSpot(6, 4),
],
isCurved: true,
colors: [Colors.blue],
dotData: FlDotData(show: false),
belowBarData: BarAreaData(show: false),
),
],
),
),
),
),
);
}
}

This code creates a basic line chart with sample data. It sets up the chart appearance and defines the data points.

5. Customizing the Chart

You can customize the chart further by tweaking its appearance, labels, and more. Explore the fl_chart documentation (https://pub.dev/packages/fl_chart) to learn about various customization options.

6. Adding Interactivity

To make your chart interactive, you can implement gestures like tap or swipe. The fl_chart library provides gesture support for charts. Refer to the documentation for details on adding interactivity.

7. Real-World Example: Stock Price Chart

As a more advanced example, let's create a stock price chart with historical data fetched from an API. We'll use the http package to make API requests.

// Import necessary packages at the top of main.dart
import 'package:http/http.dart' as http;
import 'dart:convert';

// Create a function to fetch stock price data
Future<List<FlSpot>> fetchStockPriceData() async {
final response = await http.get(Uri.parse('YOUR_API_ENDPOINT_HERE'));

if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
final List<FlSpot> spots = [];

for (var i = 0; i < data.length; i++) {
spots.add(FlSpot(i.toDouble(), data[i]['price'].toDouble()));
}

return spots;
} else {
throw Exception('Failed to load stock price data');
}
}

// Inside the LineChart widget, replace the spots with fetched data
lineBarsData: [
LineChartBarData(
spots: await fetchStockPriceData(), // Fetch and populate data
isCurved: true,
colors: [Colors.blue],
dotData: FlDotData(show: false),
belowBarData: BarAreaData(show: false),
),
],

Replace 'YOUR_API_ENDPOINT_HERE' with the actual API endpoint that provides historical stock price data in JSON format.

Conclusion

In this article, we explored how to integrate and use charts in Flutter applications. We started by setting up a Flutter project, choosing a charting library, and installing the fl_chart package. We created a basic line chart, customized it, and discussed adding interactivity. Finally, we implemented a real-world example of a stock price chart with data fetched from an API.

Charts are essential for visualizing data and providing insights in your Flutter applications. With the fl_chart library and the knowledge gained from this tutorial, you can create visually appealing and interactive charts to enhance your app's user experience.

Happy charting!

HOW TO AVOID MEMORY LEAKS IN JETPACK COMPOSE

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

Jetpack Compose is a modern Android UI toolkit introduced by Google, designed to simplify UI development and create more efficient and performant apps. While it offers numerous advantages, like a declarative UI syntax and increased developer productivity, it's not immune to memory leaks.

Memory leaks in Android can lead to sluggish performance and even app crashes. In this blog post, we'll explore the possibilities of causing memory leaks in Jetpack Compose and common reasons behind them. We'll also provide code examples and discuss strategies to prevent and fix these issues.

Understanding Memory Leaks

Before diving into Jetpack Compose-specific issues, let's briefly understand what a memory leak is. A memory leak occurs when objects that are no longer needed are not released from memory, causing a gradual increase in memory consumption over time. In Android, this is typically caused by retaining references to objects that should be garbage collected.

How to Avoid Memory Leaks in Jetpack Compose

1. Lambda Expressions and Captured Variables

Jetpack Compose heavily relies on lambda expressions and function literals. When these lambdas capture references to objects, they can unintentionally keep those objects in memory longer than necessary. This often happens when lambdas capture references to ViewModels or other long-lived objects.

@Composable
fun MyComposable(viewModel: MyViewModel) {
// This lambda captures a reference to viewModel
Button(onClick = { viewModel.doSomething() }) {
Text("Click me")
}
}

In this example, the lambda passed to Button captures a reference to the viewModel parameter. If MyComposable gets recomposed, a new instance of the lambda will be created, but it still captures the same viewModel reference. If the old MyComposable instance is no longer in use, the captured viewModel reference will keep it from being garbage collected, potentially causing a memory leak.

To avoid this, you can use the remember function to ensure that the lambda captures a stable reference:

@Composable
fun MyComposable(viewModel: MyViewModel) {
val viewModelState by remember { viewModel.state }

Button(onClick = { viewModelState.doSomething() }) {
Text("Click me")
}
}

Here, remember is used to cache the value of viewModel.state. This ensures that the lambda inside Button captures a stable reference to viewModelState. As a result, even if MyComposable is recomposed, it won't create unnecessary new references to viewModel, reducing the risk of memory leaks.

2. Composable Functions and State

Composables are functions that can rebuild when their inputs change. If you're not careful, unnecessary recompositions can lead to memory leaks. Composable functions that create and hold onto state objects, especially those with a long lifecycle, can cause memory leaks.

@Composable
fun MyComposable() {
val context = LocalContext.current
val database = Room.databaseBuilder(context, MyDatabase::class.java, "my-database").build()

// ...
}

To mitigate this, prefer creating and closing resources within a DisposableEffect:

@Composable
fun MyComposable() {
val context = LocalContext.current

DisposableEffect(Unit) {
val database = Room.databaseBuilder(context, MyDatabase::class.java, "my-database").build()
onDispose {
database.close()
}
}

// ...
}

3. Forgetting to Dispose of Observers

Jetpack Compose's LiveData and State are commonly used for observing and updating UI. However, not removing observers correctly can result in memory leaks. When a Composable is removed from the UI hierarchy, you should ensure that it no longer observes any LiveData or State.

@Composable
fun MyComposable(viewModel: MyViewModel) {
val data = viewModel.myLiveData.observeAsState()

// ...
}

To address this, use the DisposableEffect to automatically remove observers when the Composable is no longer needed:

@Composable
fun MyComposable(viewModel: MyViewModel) {
DisposableEffect(viewModel) {
val data = viewModel.myLiveData.observeAsState()
onDispose {
// Remove observers or do necessary cleanup here
}
}

// ...
}

Conclusion

Jetpack Compose is a powerful tool for building modern Android user interfaces. However, like any technology, it's essential to be aware of potential pitfalls, especially regarding memory management.

By understanding the common causes of memory leaks and following best practices, you can create efficient and performant Compose-based apps that delight your users.

INTEGRATING AND USING FIRESTORE IN FLUTTER APPS

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

Firestore is a powerful NoSQL database offered by Firebase, a platform provided by Google. It's a perfect fit for building real-time, cloud-hosted applications.

In this article, we'll explore how to integrate Firestore into a Flutter application and build a complete CRUD (Create, Read, Update, Delete) address book application. By the end of this tutorial, you'll have a fully functional address book app that allows you to manage your contacts.

Prerequisites

Before we begin, ensure you have the following prerequisites:

  • Flutter Environment: Make sure you have Flutter installed and set up on your development machine. You can get started with Flutter by following the official installation guide.

  • Firebase Account: Create a Firebase account (if you don't have one) and set up a new project on the Firebase Console.

  • FlutterFire Dependencies: We'll use the cloud_firestore package to interact with Firestore. Add the following dependency to your pubspec.yaml file:

dependencies:
flutter:
sdk: flutter
cloud_firestore: ^4.9.1

Run flutter pub get to fetch the package.

Setting up Firestore

  • Firebase Project Configuration: In your Firebase project, go to the Firebase Console and click on "Project settings." Under the "General" tab, scroll down to the "Your apps" section and click on the "Firebase SDK snippet" icon (</>) for the web app. This will provide you with a configuration snippet containing your Firebase credentials.

  • Initialize Firebase in Flutter: In your Flutter app, open the main.dart file and add the following code to initialize Firebase using the configuration snippet obtained in the previous step:

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

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}

Building the Address Book App

Now, let's start building our address book app. We'll create a simple app with the following features:

  • Display a list of contacts.

  • Add a new contact.

  • Edit an existing contact.

  • Delete a contact.

Create a Firestore Collection

In Firestore, data is organized into collections and documents. For our address book app, let's create a collection named "contacts."

final CollectionReference contactsCollection = FirebaseFirestore.instance.collection('contacts');

Create a Model Class

We'll need a model class to represent our contact. Create a file named contact.dart and define the following class:

class Contact {
final String id;
final String name;
final String phoneNumber;

Contact({required this.id, required this.name, required this.phoneNumber});
}

Create a CRUD Service

Next, let's create a service to perform CRUD operations on our Firestore collection. Create a file named crud_service.dart and implement the following methods:

import 'package:cloud_firestore/cloud_firestore.dart';

class CrudService {
// Reference to the Firestore collection
final CollectionReference contactsCollection = FirebaseFirestore.instance.collection('contacts');

Future&lt;void&gt; addContact(String name, String phoneNumber) async {
await contactsCollection.add({'name': name, 'phoneNumber': phoneNumber});
}

Future&lt;void&gt; updateContact(String id, String name, String phoneNumber) async {
await contactsCollection.doc(id).update({'name': name, 'phoneNumber': phoneNumber});
}

Future&lt;void&gt; deleteContact(String id) async {
await contactsCollection.doc(id).delete();
}
}

Implementing UI

Now, let's create the user interface for our address book app using Flutter widgets. We'll create screens for listing contacts, adding a new contact, and editing an existing contact.

Listing Contacts

import 'package:flutter/material.dart';
import 'package:your_app_name/models/contact.dart';
import 'package:your_app_name/services/crud_service.dart';

class ContactListScreen extends StatelessWidget {
final CrudService crudService = CrudService();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Contacts')),
body: StreamBuilder&lt;QuerySnapshot&gt;(
stream: crudService.contactsCollection.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}

if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}

final contacts = snapshot.data?.docs ?? [];

return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, index) {
final contact = Contact(
id: contacts[index].id,
name: contacts[index]['name'],
phoneNumber: contacts[index]['phoneNumber'],
);

return ListTile(
title: Text(contact.name),
subtitle: Text(contact.phoneNumber),
onTap: () {
// Navigate to contact details/edit screen
},
onLongPress: () {
// Delete contact
},
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Navigate to add contact screen
},
child: Icon(Icons.add),
),
);
}
}

Adding and Editing Contacts

import 'package:flutter/material.dart';
import 'package:your_app_name/models/contact.dart';
import 'package:your_app_name/services/crud_service.dart';

class AddEditContactScreen extends StatefulWidget {
final Contact? contact;

AddEditContactScreen({this.contact});

@override
_AddEditContactScreenState createState() =&gt; _AddEditContactScreenState();
}

class _AddEditContactScreenState extends State&lt;AddEditContactScreen&gt; {
final CrudService crudService = CrudService();
final _formKey = GlobalKey&lt;FormState&gt;();
late TextEditingController _nameController;
late TextEditingController _phoneNumberController;

@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.contact?.name ?? '');
_phoneNumberController = TextEditingController(text: widget.contact?.phoneNumber ?? '');
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.contact == null ? 'Add Contact' : 'Edit Contact'),
),
body: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(labelText: 'Name'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a name';
}
return null;
},
),
TextFormField(
controller: _phoneNumberController,
decoration: InputDecoration(labelText: 'Phone Number'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a phone number';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
final name = _nameController.text;
final phoneNumber = _phoneNumberController.text;

if (widget.contact == null) {
// Add contact
} else {
// Update contact
}
}
},
child: Text(widget.contact == null ? 'Add Contact' : 'Save Changes'),
),
],
),
),
);
}
}

Conclusion

In this article, we've walked through the process of integrating Firestore into a Flutter app and building a complete CRUD address book application. You've learned how to set up Firestore, create a model class, implement CRUD operations, and create the user interface for listing, adding, and editing contacts.

This is just the beginning, and you can further enhance your app by adding authentication, search functionality, and more features. Firestore and Flutter provide a powerful combination for building modern and scalable mobile applications.

Happy coding!

INTEGRATING GOOGLE MAPS IN JETPACK COMPOSE ANDROID APPS

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

Are you looking to add Google Maps integration to your Jetpack Compose Android app and display a moving vehicle on the map?

You're in the right place!

In this step-by-step guide, we'll walk you through the process of setting up Google Maps in your Android app using Jetpack Compose and adding a dynamic moving vehicle marker.

Prerequisites

Before we dive into the implementation, make sure you have the following prerequisites in place:

  • Android Studio Arctic Fox: Ensure you have the latest version of Android Studio installed.

  • Google Maps Project: Create a Google Maps project in Android Studio using the "Empty Compose Activity" template. This template automatically includes the necessary dependencies for Jetpack Compose.

  • Google Maps API Key: You'll need a Google Maps API key for your project.

Now, let's get started with the integration:

Step 1: Set Up the Android Project

  • Open Android Studio and create a new Jetpack Compose project.

  • In the build.gradle (Project) file, add the Google Maven repository:

allprojects {
repositories {
// other repositories

google()
}
}

In the build.gradle (app) file, add the dependencies for Jetpack Compose, Google Maps, and Permissions:

android {
// ...

defaultConfig {
// ...

// Add the following line
resValue "string", "google_maps_api_key", "{YOUR_API_KEY}"
}

// ...
}

dependencies {
// ...

// Google Maps

implementation "com.google.android.gms:play-services-maps:18.1.0"
implementation "com.google.maps.android:maps-ktx:3.2.1"

// Permissions
implementation "com.permissionx.guolindev:permissionx:1.7.0"
}

Replace {YOUR_API_KEY} with your actual Google Maps API key.

Step 2: Request Location Permissions

In your Compose activity or fragment, request location permissions from the user using PermissionX or any other permission library of your choice.

import com.permissionx.guolindev.PermissionX

// Inside your Composable function
PermissionX.init(this@YourActivity)
.permissions(Manifest.permission.ACCESS_FINE_LOCATION)
.request { granted, _, _ -&gt;
if (granted) {
// User granted location permission
} else {
// Handle permission denied
}
}

Step 3: Create a Map Composable

Now, let's create a Composable function to display the Google Map.

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.MapView
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions

@Composable
fun MapView() {
val mapView = rememberMapViewWithLifecycle()

AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context -&gt;
mapView.apply {
// Initialize the MapView
onCreate(null)
getMapAsync { googleMap -&gt;
// Set up Google Map settings here
val initialLocation = LatLng(37.7749, -122.4194) // Default location (San Francisco)
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(initialLocation, 12f))

// Add a marker for the vehicle
val vehicleLocation = LatLng(37.7749, -122.4194) // Example vehicle location
val vehicleMarker = MarkerOptions().position(vehicleLocation).title("Vehicle")
googleMap.addMarker(vehicleMarker)
}
}
}
)
}

Replace the default and example coordinates with the desired starting location for your map and the initial vehicle position.

Step 4: Animate the Vehicle

To animate the vehicle, you'll need to update its position periodically. You can use Handler or a timer for this purpose. Here's a simplified example of how to animate the vehicle:

import android.os.Handler
import androidx.compose.runtime.*

@Composable
fun MapWithAnimatedVehicle() {
val mapView = rememberMapViewWithLifecycle()
var vehicleLocation by remember { mutableStateOf(LatLng(37.7749, -122.4194)) }

AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context -&gt;
mapView.apply {
// Initialize the MapView
onCreate(null)
getMapAsync { googleMap -&gt;
// Set up Google Map settings here
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(vehicleLocation, 12f))

// Add a marker for the vehicle
val vehicleMarker = MarkerOptions().position(vehicleLocation).title("Vehicle")
googleMap.addMarker(vehicleMarker)

// Animate the vehicle's movement
val handler = Handler()
val runnable = object : Runnable {
override fun run() {
// Update the vehicle's position (e.g., simulate movement)
vehicleLocation = LatLng(
vehicleLocation.latitude + 0.001,
vehicleLocation.longitude + 0.001
)
googleMap.animateCamera(
CameraUpdateFactory.newLatLng(vehicleLocation)
)
handler.postDelayed(this, 1000) // Update every 1 second
}
}
handler.post(runnable)
}
}
}
)
}

This code sets up a simple animation that moves the vehicle marker by a small amount every second. You can customize this animation to fit your specific use case.

Step 5: Display the Map in Your UI

Finally, you can use the MapView or MapWithAnimatedVehicle Composable functions within your Compose UI hierarchy to display the map. For example:

@Composable
fun YourMapScreen() {
Column {
// Other Composables and UI elements
MapWithAnimatedVehicle()
// Other Composables and UI elements
}
}

That's it! You've successfully integrated Google Maps into your Jetpack Compose Android app and animated a moving vehicle marker on the map.

Conclusion

In this blog post, we've covered the basics of integrating Google Maps into your Jetpack Compose Android app and added a dynamic moving marker. You can further enhance this example by integrating location tracking, route rendering, and more, depending on your project requirements.

I hope this guide was helpful in getting you started with Google Maps in Jetpack Compose. If you have any questions or need further assistance, please don't hesitate to ask.

Happy coding!

COMMON MISTAKES DEVELOPERS MAKE WHEN DEVELOPING IOS APPS IN SWIFTUI

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

SwiftUI, introduced by Apple in 2019, has revolutionized the way developers create user interfaces for iOS apps. It offers a declarative syntax, real-time previews, and a host of powerful features. While SwiftUI makes app development more accessible, it's not without its pitfalls.

In this blog post, we'll explore some common mistakes developers make when developing iOS apps in SwiftUI and how to avoid them.

1. Neglecting to Learn SwiftUI Fundamentals

Mistake: Many developers rush into SwiftUI without adequately learning its fundamental concepts. SwiftUI requires a shift in mindset compared to UIKit, and neglecting to understand its core principles can lead to confusion and frustration.

Solution: Start with Apple's official SwiftUI tutorials and documentation. Take the time to understand concepts like Views, State, Binding, and ViewModifiers. Investing in a solid foundation will pay off in the long run.

struct ContentView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Counter: \(count)")
Button("Increment") {
count += 1
}
}
}
}

2. Using UIKit Elements in SwiftUI Views

Mistake: Mixing UIKit elements (e.g., UIWebView, UILabel) with SwiftUI views can lead to layout issues and hinder the responsiveness of your app.

Solution: Whenever possible, use SwiftUI-native components. If you need to integrate UIKit elements, encapsulate them in UIViewRepresentable or UIViewControllerRepresentable wrappers to maintain SwiftUI compatibility.

import SwiftUI
import UIKit

struct WebView: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -&gt; UIWebView {
let webView = UIWebView()
webView.loadRequest(URLRequest(url: url))
return webView
}

func updateUIView(_ uiView: UIWebView, context: Context) {
// Handle updates if needed
}
}

3. Overusing @State and Mutable State

Mistake: Using @State for every piece of data can lead to a tangled web of mutable state, making it challenging to track and manage updates.

Solution: Be selective when using @State. Reserve it for view-specific state that should persist across view updates. For temporary or global data, consider using @StateObject, @ObservedObject, or @EnvironmentObject, depending on the scope of the data.

struct ContentView: View {
@State private var count = 0
@StateObject private var userData = UserData()

var body: some View {
VStack {
Text("Counter: \(count)")
Button("Increment") {
count += 1
}
// Use userData here
}
}
}

4. Ignoring Layout and Performance Optimization

Mistake: SwiftUI abstracts many layout details, but ignoring them completely can result in poor performance and inconsistent user experiences.

Solution: Learn how SwiftUI handles layout and rendering by using tools like the frame modifier, GeometryReader, and ScrollViewReader. Optimize performance by using List for large datasets and paying attention to the use of .onAppear and .onDisappear modifiers.

List(items) { item in
Text(item.name)
.onAppear {
// Load additional data
// or perform actions when the item appears
}
}

5. Not Handling Error States and Edge Cases

Mistake: Failing to anticipate error states, empty data scenarios, or edge cases can lead to crashes or confusing user experiences.

Solution: Always consider possible failure points in your app and handle them gracefully with error views, empty state placeholders, or informative alerts.

if let data = fetchData() {
// Display data
} else {
// Show error view or alert
}

Conclusion

SwiftUI offers a powerful and modern way to build iOS apps, but like any technology, it comes with its share of possibilities to make common mistakes. By taking the time to understand SwiftUI's fundamentals, using native components, managing state wisely, optimizing layout and performance, and handling edge cases, you can avoid these pitfalls and create robust and responsive iOS apps that delight your users.

Remember, practice and continuous learning are key to mastering SwiftUI development.

A BRIEF GUIDE FOR INTEGRATING GOOGLE MAPS IN FLUTTER

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

Google Maps is a widely used tool for displaying interactive maps and location data in mobile applications. Flutter, a popular open-source UI framework, allows developers to create beautiful and functional cross-platform apps.

In this blog post, we will walk you through the process of integrating Google Maps into a Flutter app.

Prerequisites

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

  • Flutter SDK installed on your machine.

  • A Google Cloud Platform (GCP) account with the Maps JavaScript API enabled.

  • An Android or iOS emulator or a physical device for testing.

Step 1: Set Up a New Flutter Project

If you haven't already, create a new Flutter project using the following command:

flutter create google_maps_flutter_app
cd google_maps_flutter_app

Step 2: Add Dependencies for Google Maps

Open the pubspec.yaml file in your project and add the necessary dependencies for Google Maps integration:

dependencies:
flutter:
sdk: flutter
google_maps_flutter: ^2.5.0
location: ^5.0.3

After adding the dependencies, run the following command to fetch and install them:

flutter pub get

Step 3: Configure API Key

Visit the Google Cloud Console and create a new project. Then, enable the "Maps JavaScript API" for your project.

Once done, generate an API key for your application.

Open the AndroidManifest.xml file located at android/app/src/main/AndroidManifest.xml and add your API key within the <application> tag:

&lt;meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY_HERE"/&gt;

For iOS, open the AppDelegate.swift file located at ios/Runner/AppDelegate.swift and add the following code within the application(_:didFinishLaunchingWithOptions:) method:

GMSServices.provideAPIKey("YOUR_API_KEY_HERE")

Step 4: Set Up Permissions

To use location services on Android and iOS, you need to configure the necessary permissions. Open the AndroidManifest.xml file and add the following permissions:

&lt;uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /&gt;
&lt;uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /&gt;

For iOS, open the Info.plist file located at ios/Runner/Info.plist and add the following keys:

&lt;key&gt;NSLocationWhenInUseUsageDescription&lt;/key&gt;
&lt;string&gt;We need your location to display on the map.&lt;/string&gt;&lt;key&gt;NSLocationAlwaysUsageDescription&lt;/key&gt;
&lt;string&gt;We need your location to display on the map.&lt;/string&gt;

Step 5: Implement Google Maps

Create a new Dart file named google_maps_screen.dart in your lib directory. In this file, import the necessary packages:

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:location/location.dart';

Now, create a stateful widget called GoogleMapsScreen:

class GoogleMapsScreen extends StatefulWidget {
@override
_GoogleMapsScreenState createState() =&gt; _GoogleMapsScreenState();
}

class _GoogleMapsScreenState extends State&lt;GoogleMapsScreen&gt; {
GoogleMapController? _mapController;
Location _location = Location();
LatLng _initialPosition = LatLng(0, 0);

@override
void initState() {
super.initState();
_location.getLocation().then((locationData) {
setState(() {
_initialPosition = LatLng(locationData.latitude!, locationData.longitude!);
});
});
}

void _onMapCreated(GoogleMapController controller) {
_mapController = controller;
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Google Maps Integration'),
),
body: GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(target: _initialPosition, zoom: 15),
myLocationEnabled: true,
compassEnabled: true,
),
);
}
}

Step 6: Add Navigation

Open the main.dart file and modify the main function to point to the GoogleMapsScreen widget:

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Google Maps Flutter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: GoogleMapsScreen(),
);
}
}

Step 7: Run the App

Now you're ready to test your Google Maps integration! Run your Flutter app using your preferred emulator or physical device:

flutter run

Your app should open, displaying a map centered around your current location.

Congratulations! You've successfully integrated Google Maps into your Flutter app. You can now customize the map's appearance, add markers, and implement various interactive features to enhance the user experience.

Remember to refer to the official Google Maps Flutter documentation for more advanced features and customization options.

Happy coding!

COMMON MISTAKES WHILE USING JETPACK COMPOSE

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

Jetpack Compose has revolutionized the way we build user interfaces for Android applications. With its declarative syntax and efficient UI updates, it offers a fresh approach to UI development. However, like any technology, using Jetpack Compose effectively requires a solid understanding of its principles and potential pitfalls.

In this blog, we'll explore some common mistakes developers might make when working with Jetpack Compose and how to avoid them.

Mistake 1: Incorrect Usage of Modifier Order

Modifiers in Jetpack Compose are used to apply various transformations and styling to UI elements. However, the order in which you apply these modifiers matters. For example, consider the following code:

Text(
text = "Hello, World!",
modifier = Modifier
.padding(16.dp)
.background(Color.Blue)
)

In this code, the padding modifier is applied before the background modifier. This means the background color might not be applied as expected because the padding could cover it up. To fix this, reverse the order of the modifiers:

Text(
text = "Hello, World!",
modifier = Modifier
.background(Color.Blue)
.padding(16.dp)
)

Always make sure to carefully order your modifiers based on the effect you want to achieve.

Mistake 2: Excessive Re-Composition

One of the key advantages of Jetpack Compose is its ability to automatically handle UI updates through recomposition. However, excessive recomposition can lead to performance issues. Avoid unnecessary recomposition by ensuring that only the parts of the UI that actually need to be updated are recomposed.

Avoid using functions with side effects, such as network requests or database queries, directly within a composable function. Instead, use the remember and derivedStateOf functions to manage state and perform these operations outside the composable scope.

val data by remember { mutableStateOf(fetchData()) }

Mistake 3: Misusing State Management in Jetpack Compose

Jetpack Compose provides several options for managing state, such as mutableStateOf, remember, and viewModel. Choosing the right state management approach for your use case is crucial.

Using mutableStateOf inappropriately can lead to unexpected behavior. For instance, avoid using mutableStateOf for complex objects like lists. Instead, use the state parameter of the LazyColumn or LazyRow composables.

LazyColumn(
state = rememberLazyListState(),
content = { /* items here */ }
)

For more advanced scenarios, consider using the viewModel and stateFlow combination, which provides a solid architecture for managing state across different parts of your application.

Mistake 4: Ignoring Composable Constraints

Composables in Jetpack Compose are designed to be flexible and responsive to layout constraints. Ignoring these constraints can lead to UI elements overflowing or not being displayed correctly.

When working with layouts like Column or Row, ensure that you specify the modifier correctly to ensure proper spacing and alignment. Additionally, use the weight modifier to distribute available space proportionally among child elements.

Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
) {
Text("Top Text")
Text("Bottom Text")
}

Mistake 5: Inefficient List Handling

Working with lists in Jetpack Compose is quite different from traditional Android views. Mistakes can arise from using the wrong composables or inefficiently handling list updates.

Prefer using LazyColumn and LazyRow for lists, as they load only the visible items, resulting in better performance for larger lists. Use the items parameter of LazyColumn to efficiently render dynamic lists:

LazyColumn {
items(itemsList) { item -&gt;
Text(text = item)
}
}

When updating lists, avoid using the += or -= operators with mutable lists. Instead, use the appropriate list modification functions to ensure proper recomposition:

val updatedList = currentList.toMutableList()
updatedList.add(newItem)

Conclusion

Jetpack Compose is an exciting technology that simplifies UI development for Android applications. However, avoiding common mistakes is essential for a smooth development experience and optimal performance. By understanding and addressing the issues outlined in this guide, you can make the most out of Jetpack Compose and create stunning, efficient user interfaces for your Android apps.

Remember, learning from mistakes is part of the development journey. Happy coding with Jetpack Compose!

Happy Coding!

Note: The code snippets provided in this blog are for illustrative purposes and might not represent complete working examples. Always refer to the official Jetpack Compose documentation for accurate and up-to-date information.

END-TO-END TESTING OF FLUTTER APPS WITH FLUTTER_DRIVER

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

End-to-end (E2E) testing is a critical part of the app development process. It helps ensure that your Flutter app functions correctly from the user's perspective, simulating real-world interactions and scenarios. Flutter provides a powerful tool called flutter_driver for performing E2E testing.

In this blog post, we will delve into the world of flutter_driver and learn how to effectively perform E2E testing for your Flutter app.

1. Introduction to flutter_driver

flutter_driver is a Flutter package that allows you to write and execute E2E tests on your Flutter app. It provides APIs for interacting with the app and querying the widget tree. The tests are written in Dart and can simulate user interactions, such as tapping buttons, entering text, and verifying UI elements' states.

2. Setting up the Test Environment

To get started with E2E testing using flutter_driver, follow these steps:

Step 1: Add Dependencies

In your pubspec.yaml file, add the following dependencies:

dev_dependencies:
flutter_driver:
sdk: flutter
test: any

Step 2: Create a Test Driver File

Create a Dart file (e.g., app_test.dart) in your test directory. This file will define your E2E tests.

Step 3: Start the Test App

Before running E2E tests, you need to start your app in a special mode that's suitable for testing. Run the following command in your terminal:

flutter drive --target=test_driver/app.dart

3. Writing E2E Tests

Let's create a simple E2E test to demonstrate the capabilities of flutter_driver. Our test scenario will involve tapping a button and verifying that a specific text appears.

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

void main() {
group('App E2E Test', () {
FlutterDriver driver;

// Connect to the Flutter app before running the tests.
setUpAll(() async {
driver = await FlutterDriver.connect();
});

// Close the connection to the Flutter app after tests are done.
tearDownAll(() async {
if (driver != null) {
driver.close();
}
});

test('Verify Button Tap', () async {
// Find the button by its label.
final buttonFinder = find.byValueKey('myButton');

// Tap the button.
await driver.tap(buttonFinder);

// Find the text by its value.
final textFinder = find.text('Button Tapped');

// Verify that the expected text appears.
expect(await driver.getText(textFinder), 'Button Tapped');
});
});
}

In this example, we've defined a simple test that interacts with a button and verifies the appearance of specific text.

4. Running E2E Tests

Run your E2E tests using the following command:

flutter drive --target=test_driver/app.dart

This will execute the tests defined in your app_test.dart file.

5. Analyzing Test Results

After the tests have run, the terminal will display the test results. You'll see information about passed and failed tests, along with any error messages or stack traces.

6. Best Practices for E2E Testing

  • Isolation: E2E tests should be independent of each other and the testing environment. Avoid relying on the state of previous tests.

  • Use Keys: Assign keys to widgets that you want to interact with in E2E tests. This helps maintain stability even when widget positions change.

  • Clear States: Ensure your app is in a known state before each test. This may involve resetting the app's state or navigating to a specific screen.

  • Regular Maintenance: E2E tests can become fragile if not maintained. Update tests when UI changes occur to prevent false positives/negatives.

  • Limit Flakiness: Use await judiciously to ensure the app has stabilized before performing verifications. This can help reduce test flakiness.

E2E testing with flutter_driver is a powerful way to ensure the quality of your Flutter apps. By writing comprehensive tests that mimic user interactions, you can catch bugs and regressions early in the development process, leading to a more robust and reliable app.

In this blog post, we've covered the basics of setting up flutter_driver, writing tests, running them, and best practices to follow. With this knowledge, you can start incorporating E2E testing into your Flutter development workflow.

Happy testing!

USING MONKEYRUNNER TO TEST ANDROID APPS

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

Mobile app testing is an essential part of the development process to ensure that your app functions correctly across various devices and scenarios. However, manual testing can be time-consuming and error-prone. To streamline the testing process, developers often turn to automation tools. One such tool is MonkeyRunner, a script-based testing framework for Android apps.

In this blog post, we'll explore how to use MonkeyRunner to automate the testing of Android apps.

What is MonkeyRunner?

MonkeyRunner is a part of the Android SDK that provides a way to write scripts to automate tasks and test Android apps on physical devices or emulators. It simulates user interactions, such as tapping, swiping, and pressing hardware buttons, to mimic real-world usage scenarios.

Setting Up the Environment

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

  • Android SDK: Install the Android SDK and add the tools and platform-tools directories to your system's PATH.

  • Python: MonkeyRunner scripts are written in Python. Ensure you have Python installed on your system.

Writing the MonkeyRunner Script

Let's write a basic MonkeyRunner script in Python that interacts with an Android app. This script will launch the app, simulate touch events, and capture screenshots.

Step 1: Create the MonkeyRunner Script

Create a new Python (.py) file in your project directory and name it app_test.py.

Step 2: Import MonkeyRunner Modules

In your app_test.py file, import the necessary MonkeyRunner modules:

from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice

Step 3: Connect to the Device

Connect to the Android device or emulator using MonkeyRunner:

device = MonkeyRunner.waitForConnection()

Step 4: Launch the App

Launch the target app on the connected device:

package_name = "com.example.myapp"  # Replace with your app's package name
activity_name = "com.example.myapp.MainActivity" # Replace with the main activity's name

device.startActivity(component=package_name + "/" + activity_name)
MonkeyRunner.sleep(5) # Wait for the app to launch (adjust the time as needed)

Step 5: Simulate Touch Events

Simulate touch events on the app screen:

device.touch(500, 1000, MonkeyDevice.DOWN_AND_UP)  # Replace with desired coordinates
MonkeyRunner.sleep(2) # Wait for 2 seconds

Step 6: Capture Screenshots

Capture screenshots of the app:

screenshot = device.takeSnapshot()
screenshot_path = "path/to/save/screenshot.png"
screenshot.writeToFile(screenshot_path, "png")

Step 7: Clean Up

Close the app and disconnect from the device:

device.shell("am force-stop " + package_name)
device.dispose()

Running the MonkeyRunner Script

To run the MonkeyRunner script, execute the following command in your terminal:

monkeyrunner app_test.py

This will execute the script, simulating touch events on the target app and capturing screenshots.

Conclusion

Automating Android app testing with MonkeyRunner can save you time and effort while ensuring your app's functionality across various scenarios. By integrating MonkeyRunner scripts, you can harness the power of one of the best automation tools to create a seamless testing process for your Android apps. Remember to customize the scripts according to your app's specific features and requirements.

Happy testing!

COLD START, WARM START AND HOT START IN ANDROID APPS

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

In the world of mobile app development, creating a seamless user experience is paramount. One of the critical factors that contribute to this experience is how quickly an app starts up and becomes responsive. This process is known as app start-up, and it can be categorized into three phases: Cold Start, Warm Start, and Hot Start.

In this blog, we will delve into each of these start-up phases, explore their implications on user experience, and provide insights into how to improve them.

Android App start scenarios

When you launch an Android app, there are three possible scenarios:

  • Cold start: The app is starting from scratch. This is the slowest type of launch, as the system has to create the app's process, load its code and resources, and initialize its components.

  • Warm start: The app's process is already running in the background. In this case, the system only needs to bring the app's activity to the foreground. This is faster than a cold start, but it is still slower than a hot start.

  • Hot start: The app's activity is already in the foreground. In this case, the system does not need to do anything, as the app is already running. This is the fastest type of launch.

The following sections will discuss each of these types of launch in more detail, and provide tips on how to improve them.

Cold start

A cold start occurs when the app is launched for the first time after installation or after the system has killed the app process. The following are some of the steps involved in a cold start:

  • The system creates the app's process.

  • The system loads the app's code and resources.

  • The system initializes the app's components.

  • The app's main activity is displayed.

The cold start is the slowest type of launch because it involves loading all of the app's code and resources from scratch. This can take a significant amount of time, especially for large apps.

Ideally the app should complete a cold start in 500 milli seconds or less. That could be challenging sometimes, but make sure the app does the cold start in under 5 seconds. There are a number of things you can do to improve the cold start time of your app:

  • Use lazy loading: Lazy loading means loading resources only when they are needed. This can help to reduce the amount of time it takes to load the app.

  • Use a profiler: A profiler can help you to identify the parts of your app that are taking the most time to load. This can help you to focus your optimization efforts on the most critical areas.

  • Use a caching mechanism: A caching mechanism can store frequently used resources in memory, so that they do not have to be loaded from disk each time the app is launched.

  • Use a custom launcher: A custom launcher can preload the app's resources in the background before the app is launched. This can significantly reduce the cold start time.

Warm start

A warm start occurs when the app's process is already running in the background. In this case, the system only needs to bring the app's activity to the foreground. This is faster than a cold start, but it is still slower than a hot start.

The following are some of the steps involved in a warm start:

  • The system finds the app's process.

  • The system brings the app's activity to the foreground.

The warm start is faster than a cold start because the app's process is already running. However, the system still needs to bring the app's activity to the foreground, which can take some time.

Ideally the app should complete a warm start in 200 milli seconds or less. In any case, try not to breach the 2 seconds window. There are a number of things you can do to improve the warm start time of your app:

  • Use a profiler: A profiler can help you to identify the parts of your app that are taking the most time to bring to the foreground. This can help you to focus your optimization efforts on the most critical areas.

  • Use a caching mechanism: A caching mechanism can store frequently used activities in memory, so that they do not have to be recreated each time the app is launched.

  • Use a custom launcher: A custom launcher can preload the app's activities in the background before the app is launched. This can significantly reduce the warm start time.

Hot start

A hot start occurs when the app's activity is already in the foreground. In this case, the system does not need to do anything, as the app is already running. This is the fastest type of launch.

There is not much you can do to improve the hot start time of your app, as it is already running. However, you can take steps to prevent the app from being killed by the system, such as using a foreground service or a wake lock. Ideally the app should complete a hot start in 100 milli seconds or less, or in a worst case scenario, under 1.5 seconds.

Conclusion

The cold start, warm start, and hot start are the three different types of app launches in Android. The cold start is the slowest type of launch, while the hot start is the fastest.

There are a number of things you can do to improve the launch time of your app, such as using lazy loading, caching, and a custom launcher.

I hope this blog post has been helpful. If you have any questions, please feel free to leave a comment below.

COMBINE: A DECLARATIVE API FOR ASYNCHRONOUS DATA PROCESSING IN SWIFT

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

Combine is a framework for Swift introduced by Apple in 2019 that provides a declarative API. This makes it ideal for working with asynchronous data, such as network requests and user input. Combine is also a powerful tool for building reactive user interfaces.

In this blog post, we will take a look at the basics of Combine, including publishers, subscribers, and operators. We will also see how Combine can be used to build asynchronous applications and reactive user interfaces.

What is Combine?

Combine is a reactive programming framework that provides a declarative API for processing values over time. This means that you can describe the desired behaviour of your code without having to worry about the details of how it will be implemented.

Combine is based on the following concepts:

  • Publishers: Publishers emit values over time. They can be anything from network requests to user input.

  • Subscribers: Subscribers receive values from publishers. They can do things like map values, filter values, and perform other operations.

  • Operators: Operators are functions that combine publishers and subscribers. They can be used to perform common tasks, such as combining multiple publishers, filtering values, and retrying failed requests.

Using Combine to Build Asynchronous Applications in Swift

Combine is ideal for building asynchronous applications. This is because it provides a way to handle asynchronous events in a declarative way. For example, you can use Combine to make a network request and then subscribe to the response. The subscriber can then handle the response, such as mapping it to a model or displaying it in a user interface.

Here is an example of how to use Combine to make a network request:

let publisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.myhost.com")!)

publisher.subscribe(on: RunLoop.main) { data, _, error in
if let data = data {
let json = try JSONDecoder().decode(MyJSONModel.self, from: data)
// Do something with the model
} else if let error = error {
// Handle the error
}
}

This code creates a publisher that emits the response data from the network request. The subscriber then handles the response data, either mapping it to a model or displaying it in a user interface.

Using Combine to Build Reactive User Interfaces

Combine can also be used to build reactive user interfaces. This is because it provides a way to update user interfaces in response to changes in data. For example, you can use Combine to subscribe to a publisher that emits the current user location. The subscriber can then update the user interface to display the user's location.

Here is an example of how to use Combine to update a user interface with the current user location:

let publisher = locationManager.publisher(for: .location)

publisher.subscribe(on: RunLoop.main) { location in
// Update the user interface with the new location
}

This code creates a publisher that emits the current user location. The subscriber then updates the user interface to display the user's location.

Using custom Combine implementation

Let us take a look at using PassthroughSubject to implement asynchronous declarative API.

A PassthroughSubject is a type of publisher in Combine that emits any value that is sent to it. It does not have an initial value or a buffer of the most recently-published element. This makes it ideal for use in situations where you need to react to changes in data as they happen.

import Combine

let subject = PassthroughSubject&lt;String, Never&gt;()

subject.sink { string in
print(string)
}

subject.send("Hello, world!")
subject.send("This is a second message")

Here, the first line imports the Combine framework. This is needed to use the PassthroughSubject and sink operators.

The second line creates a PassthroughSubject publisher. This publisher will emit any string that is sent to it.

The third line attaches a sink subscriber to the PassthroughSubject publisher. The sink subscriber will print each string that is emitted by the publisher to the console.

The fourth and fifth lines send two strings to the PassthroughSubject publisher. These strings will be printed to the console by the sink subscriber.

Conclusion

Combine is a framework that provides a declarative API for processing values over time. This makes it ideal for working with asynchronous data and building reactive user interfaces. If you are new to Combine, I encourage you to check out the official documentation and tutorials.

I hope this blog post has given you a basic understanding of Combine. If you have any questions, please feel free to leave a comment below.

A GUIDE ON FLUTTER ANIMATIONS

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

Animations play a vital role in creating engaging and visually appealing user interfaces in mobile applications. Flutter, a popular open-source UI framework by Google, offers a robust set of tools for creating smooth and expressive animations.

In this comprehensive guide, we'll explore the world of Flutter animations, from the basics to more advanced techniques, accompanied by code samples to help you get started.

1. Introduction to Flutter Animations

Why Animations Matter

Animations provide a more dynamic and engaging user experience, guiding users through interface changes and interactions. They help convey important information, enhance the overall aesthetic of the app, and make interactions more intuitive.

Types of Animations in Flutter

Flutter offers several animation types:

  • Implicit Animations: These animations are built into existing widgets and can be triggered using widget properties, like AnimatedContainer or AnimatedOpacity.

  • Tween Animations: These animations interpolate between two values over a specified duration using Tween objects.

  • Physics-Based Animations: These animations simulate real-world physics, like springs or flings, to create natural-looking motion.

  • Custom Animations: For more complex scenarios, you can create your own custom animations using CustomPainter and AnimationController.

In this guide, we'll cover examples from each category to give you a well-rounded understanding of Flutter animations.

2. Basic Animations

Animated Container

The AnimatedContainer widget is a straightforward way to animate changes to a container's properties, such as its size, color, and alignment.

class BasicAnimatedContainer extends StatefulWidget {
@override
_BasicAnimatedContainerState createState() =&gt; _BasicAnimatedContainerState();
}

class _BasicAnimatedContainerState extends State&lt;BasicAnimatedContainer&gt; {
double _width = 100.0;
double _height = 100.0;
Color _color = Colors.blue;

void _animateContainer() {
setState(() {
_width = 200.0;
_height = 200.0;
_color = Colors.red;
});
}

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _animateContainer,
child: Center(
child: AnimatedContainer(
duration: Duration(seconds: 1),
width: _width,
height: _height,
color: _color,
),
),
);
}
}

Animated Opacity

The AnimatedOpacity widget allows you to animate the opacity of a widget, making it appear or disappear smoothly.

class BasicAnimatedOpacity extends StatefulWidget {
@override
_BasicAnimatedOpacityState createState() =&gt; _BasicAnimatedOpacityState();
}

class _BasicAnimatedOpacityState extends State&lt;BasicAnimatedOpacity&gt; {
bool _visible = true;

void _toggleVisibility() {
setState(() {
_visible = !_visible;
});
}

@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: &lt;Widget&gt;[
AnimatedOpacity(
duration: Duration(seconds: 1),
opacity: _visible ? 1.0 : 0.0,
child: FlutterLogo(size: 150.0),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _toggleVisibility,
child: Text(_visible ? "Hide Logo" : "Show Logo"),
),
],
);
}
}

3. Tween Animations

Animating Widgets with Tween

Tween animations interpolate between two values over a specified duration. Here's an example of animating the position of a widget using a Tween:

class TweenAnimation extends StatefulWidget {
@override
_TweenAnimationState createState() =&gt; _TweenAnimationState();
}

class _TweenAnimationState extends State&lt;TweenAnimation&gt; {
double _endValue = 200.0;

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
_endValue = _endValue == 200.0 ? 100.0 : 200.0;
});
},
child: Center(
child: TweenAnimationBuilder(
tween: Tween&lt;double&gt;(begin: 100.0, end: _endValue),
duration: Duration(seconds: 1),
builder: (BuildContext context, double value, Widget? child) {
return Container(
width: value,
height: value,
color: Colors.blue,
);
},
),
),
);
}
}

Tween Animation Builder

The TweenAnimationBuilder widget is a versatile tool for building animations with Tweens. It allows you to define the tween, duration, and a builder function to create the animated widget.

4. Physics-Based Animations

Using AnimatedBuilder with Curves

Curves define the rate of change in an animation, affecting its acceleration and deceleration. The CurvedAnimation class allows you to apply curves to your animations. Here's an example of using AnimatedBuilder with a curve:

class CurvedAnimationDemo extends StatefulWidget {
@override
_CurvedAnimationDemoState createState() =&gt; _CurvedAnimationDemoState();
}

class _CurvedAnimationDemoState extends State&lt;CurvedAnimationDemo&gt;
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation&lt;double&gt; _animation;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);
final Animation curveAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_animation = Tween&lt;double&gt;(begin: 0, end: 200).animate(curveAnimation);
_controller.forward();
}

@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _animation,
builder: (BuildContext context, Widget? child) {
return Container(
width: _animation.value,
height: 100,
color: Colors.blue,
);
},
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

Creating a Spring Animation

Spring animations simulate the behavior of a spring, creating a bounce-like effect. Flutter provides the SpringSimulation class for this purpose. Here's an example of creating a spring animation:

class SpringAnimationDemo extends StatefulWidget {
@override
_SpringAnimationDemoState createState() =&gt; _SpringAnimationDemoState();
}

class _SpringAnimationDemoState extends State&lt;SpringAnimationDemo&gt;
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation&lt;Offset&gt; _animation;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);

final SpringDescription spring = SpringDescription(
mass: 1,
stiffness: 500,
damping: 20,
);

final SpringSimulation springSimulation = SpringSimulation(
spring,
_controller.value,
1, // The target value
0, // The velocity
);

_animation = Tween&lt;Offset&gt;(begin: Offset.zero, end: Offset(2, 0))
.animate(_controller);

_controller.animateWith(springSimulation);
}

@override
Widget build(BuildContext context) {
return Center(
child: SlideTransition(
position: _animation,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

5. Complex Animations

Staggered Animations

Staggered animations involve animating multiple widgets with different delays, creating an appealing sequence. The StaggeredAnimation class manages this behavior. Here's an example:

class StaggeredAnimationDemo extends StatefulWidget {
@override
_StaggeredAnimationDemoState createState() =&gt; _StaggeredAnimationDemoState();
}

class _StaggeredAnimationDemoState extends State&lt;StaggeredAnimationDemo&gt;
with TickerProviderStateMixin {
late AnimationController _controller;
late Animation&lt;double&gt; _animation;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);

final StaggeredAnimation staggeredAnimation = StaggeredAnimation(
controller: _controller,
itemCount: 3,
);

_animation = Tween&lt;double&gt;(begin: 0, end: 200).animate(staggeredAnimation);

_controller.forward();
}

@override
Widget build(BuildContext context) {
return Center(
child: ListView.builder(
itemCount: 3,
itemBuilder: (BuildContext context, int index) {
return FadeTransition(
opacity: _animation,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: _animation.value,
height: 100,
color: Colors.blue,
),
),
);
},
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

class StaggeredAnimation extends Animatable&lt;double&gt; {
final AnimationController controller;
final int itemCount;

StaggeredAnimation({
required this.controller,
required this.itemCount,
}) : super();

@override
double transform(double t) {
int itemCount = this.itemCount;
double fraction = 1.0 / itemCount;
return (t * itemCount).clamp(0.0, itemCount - 1).toDouble() * fraction;
}
}

Hero Animations

Hero animations are used to smoothly transition a widget between two screens. They provide a seamless experience as the widget scales and moves from one screen to another. Here's an example:

class HeroAnimationDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute&lt;void&gt;(
builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Hero Animation'),
),
body: Center(
child: Hero(
tag: 'hero-tag',
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
);
},
),
);
},
child: Scaffold(
appBar: AppBar(
title: const Text('Hero Animation'),
),
body: Center(
child: Hero(
tag: 'hero-tag',
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
),
),
);
}
}

6. Implicit Animations

Animated CrossFade

The AnimatedCrossFade widget smoothly transitions between two children while crossfading between them. It's useful for scenarios like toggling between two pieces of content.

class CrossFadeDemo extends StatefulWidget {
@override
_CrossFadeDemoState createState() =&gt; _CrossFadeDemoState();
}

class _CrossFadeDemoState extends State&lt;CrossFadeDemo&gt; {
bool _showFirst = true;

void _toggle() {
setState(() {
_showFirst = !_showFirst;
});
}

@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: &lt;Widget&gt;[
AnimatedCrossFade(
firstChild: FlutterLogo(size: 150),
secondChild: Container(color: Colors.blue, width: 150, height: 150),
crossFadeState:
_showFirst ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: Duration(seconds: 1),
),
ElevatedButton(
onPressed: _toggle,
child: Text(_showFirst ? 'Show Second' : 'Show First'),
),
],
);
}
}

Animated Switcher

The AnimatedSwitcher widget allows smooth transitions between different children based on a key. It's commonly used for transitions like swapping widgets.

class SwitcherDemo extends StatefulWidget {
@override
_SwitcherDemoState createState() =&gt; _SwitcherDemoState();
}

class _SwitcherDemoState extends State&lt;SwitcherDemo&gt; {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: &lt;Widget&gt;[
AnimatedSwitcher(
duration: Duration(seconds: 1),
child: Text(
'$_counter',
key: ValueKey&lt;int&gt;(_counter),
style: TextStyle(fontSize: 48),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
);
}
}

7. Custom Animations

CustomPainter and AnimationController

The combination of CustomPainter and AnimationController allows you to create complex animations and draw custom shapes. Here's an example of a rotating custom animation using CustomPainter:

class CustomPainterAnimation extends StatefulWidget {
@override
_CustomPainterAnimationState createState() =&gt; _CustomPainterAnimationState();
}

class _CustomPainterAnimationState extends State&lt;CustomPainterAnimation&gt;
with SingleTickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
)..repeat();
}

@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return CustomPaint(
painter: RotatingPainter(_controller.value),
child: Container(width: 150, height: 150),
);
},
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

class RotatingPainter extends CustomPainter {
final double rotation;

RotatingPainter(this.rotation);

@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
canvas.rotate(rotation * 2 * pi);
final rect = Rect.fromCenter(
center: Offset(0, 0),
width: size.width * 0.8,
height: size.height * 0.8,
);
final paint = Paint()..color = Colors.blue;
canvas.drawRect(rect, paint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}

Creating a Flip Card Animation

Using a combination of Transform, GestureDetector, and AnimationController, you can create a flip card animation.

class FlipCardDemo extends StatefulWidget {
@override
_FlipCardDemoState createState() =&gt; _FlipCardDemoState();
}

class _FlipCardDemoState extends State&lt;FlipCardDemo&gt;
with SingleTickerProviderStateMixin {
late AnimationController _controller;
bool _isFront = true;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
);
}

void _flipCard() {
if (_isFront) {
_controller.forward();
} else {
_controller.reverse();
}
_isFront = !_isFront;
}

@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: _flipCard,
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
final double rotationValue = _controller.value;
final double rotationAngle = _isFront ? rotationValue : (1 - rotationValue);
final frontRotation = Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(pi * rotationAngle);
final backRotation = Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(pi * (rotationAngle - 1));
return Stack(
children: [
_buildCard(frontRotation, 'Front', Colors.blue),
_buildCard(backRotation, 'Back', Colors.red),
],
);
},
),
),
);
}

Widget _buildCard(Matrix4 transform, String text, Color color) {
return Center(
child: Transform(
transform: transform,
alignment: Alignment.center,
child: Container(
width: 200,
height: 300,
color: color,
alignment: Alignment.center,
child: Text(
text,
style: TextStyle(fontSize: 24, color: Colors.white),
),
),
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

8. Performance Optimization

Using the AnimationController's vsync

When creating an AnimationController, it's essential to provide a vsync parameter. This parameter helps in syncing the animation frame rate with the device's refresh rate, enhancing performance and reducing unnecessary updates.

class VsyncAnimation extends StatefulWidget {
@override
_VsyncAnimationState createState() =&gt; _VsyncAnimationState();
}

class _VsyncAnimationState extends State&lt;VsyncAnimation&gt;
with SingleTickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, // Pass `this` as the vsync parameter
duration: Duration(seconds: 2),
);
}

// ...
}

Avoiding Unnecessary Rebuilds

To avoid unnecessary rebuilds of widgets, you can use AnimatedBuilder or ValueListenableBuilder. These widgets rebuild only when the animation value changes, improving overall performance.

class AvoidRebuildsDemo extends StatefulWidget {
@override
_AvoidRebuildsDemoState createState() =&gt; _AvoidRebuildsDemoState();
}

class _AvoidRebuildsDemoState extends State&lt;AvoidRebuildsDemo&gt;
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation&lt;double&gt; _animation;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
);

_animation = Tween&lt;double&gt;(begin: 0, end: 1).animate(_controller);
}

@override
Widget build(BuildContext context) {
return Center(
child: ValueListenableBuilder(
valueListenable: _animation,
builder: (BuildContext context, double value, Widget? child) {
return Transform.scale(
scale: value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
);
},
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

9. Chaining and Sequencing Animations

Using Future.delayed

You can chain animations by using Future.delayed. This creates a delayed effect, allowing one animation to start after the previous one completes.

class DelayedAnimationDemo extends StatefulWidget {
@override
_DelayedAnimationDemoState createState() =&gt; _DelayedAnimationDemoState();
}

class _DelayedAnimationDemoState extends State&lt;DelayedAnimationDemo&gt;
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation&lt;double&gt; _animation1;
late Animation&lt;double&gt; _animation2;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);

_animation1 = Tween&lt;double&gt;(begin: 0, end: 1).animate(_controller);

_animation2 = Tween&lt;double&gt;(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1.0), // Starts after the first animation
),
);

_controller.forward();
}

@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: &lt;Widget&gt;[
ScaleTransition(
scale: _animation1,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
SizedBox(height: 20),
ScaleTransition(
scale: _animation2,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
],
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

Using AnimationController's addListener

The addListener method of AnimationController can be used to sequence animations, triggering the second animation when the first animation completes.

class SequenceAnimationDemo extends StatefulWidget {
@override
_SequenceAnimationDemoState createState() =&gt; _SequenceAnimationDemoState();
}

class _SequenceAnimationDemoState extends State&lt;SequenceAnimationDemo&gt;
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation&lt;double&gt; _animation1;
late Animation&lt;double&gt; _animation2;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);

_animation1 = Tween&lt;double&gt;(begin: 0, end: 1).animate(_controller)
..addListener(() {
if (_animation1.isCompleted) {
_controller.reset(); // Reset the controller to restart
_controller.forward(); // Start the second animation
}
});

_animation2 = Tween&lt;double&gt;(begin: 0, end: 1).animate(_controller);

_controller.forward();
}

@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: &lt;Widget&gt;[
ScaleTransition(
scale: _animation1,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
SizedBox(height: 20),
ScaleTransition(
scale: _animation2,
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
],
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

10. Conclusion and Further Learning

Flutter's animation capabilities allow you to create stunning, dynamic user interfaces that engage users and enhance their experience. This guide covered a wide range of animation techniques, from basic animations and tween animations to physics-based simulations and complex custom animations.

As you continue your journey with Flutter animations, consider exploring more advanced topics like Flare animations for vector graphics, using Rive for more complex animations, and experimenting with implicit animations for seamless UI changes.

Remember, mastering Flutter animations takes practice and experimentation. With dedication and creativity, you can bring your app's UI to life and create memorable user experiences that leave a lasting impression.

Happy animating! 🚀

Note: The code samples provided in this blog post are simplified for illustrative purposes. Actual implementation may require additional considerations and optimizations.

INTRODUCTION TO STATE MANAGEMENT IN SWIFTUI: @STATE, @STATEOBJECT AND @OBSERVEDOBJECT

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

SwiftUI is a powerful framework for building user interfaces for Apple devices. However, one of the challenges of using SwiftUI is managing state. State is the data that changes over time in your app, such as the current user's location or the contents of a shopping cart.

Using @State property wrapper in SwiftUI

There are a few different ways to manage state in SwiftUI. The simplest way is to use the @State property wrapper. The @State property wrapper allows you to store a value that can be changed within a view. When the value changes, SwiftUI will automatically update the view.

For example, let's say we have a view that displays a counter. We can use the @State property wrapper to store the current value of the counter. When the user taps a button, we can increment the counter value and then update the view.

struct CounterView: View {
@State private var counter = 0

var body: some View {
Button("Increment") {
counter += 1
}
Text("\(counter)")
}
}

The @State property wrapper is a great way to manage simple state in SwiftUI.

However, some of the limitations of using @State are,

  • @State properties can only be used in structs. This means that you can't use @State properties in classes or enums.

  • @State properties can't be used to store complex objects. This means that you can't store objects that contain functions, closures, or other complex types in a @State property.

  • @State properties can't be changed from outside the view. This means that you can't change the value of a @State property from another view or from code that isn't part of the view hierarchy.

Using @StateObject and @ObservedObject

The code below shows how to use the @StateObject and @ObservedObject property wrappers to manage state in SwiftUI.

The GameProgress class is an ObservableObject class. This means that it conforms to the ObservableObject protocol, which allows it to be observed by other views. The points property in the GameProgress class is marked with the @Published property wrapper.

This means that any changes to the value of the points property will be automatically published to any views that are observing it.

The ButtonView struct is a view that observes the progress property. The progress property is marked with the @ObservedObject property wrapper, which tells SwiftUI that the view should observe the value of the property and update itself whenever the value changes. The ButtonView struct has a button that increments the value of the points property. When the button is tapped, the points property is incremented and the InnerView struct is updated to reflect the change.

The ContentView struct is the main view of the app. It has a progress property that is an instance of the GameProgress class. The progress property is marked with the @StateObject property wrapper, which tells SwiftUI that the property is owned by the ContentView view. The ContentView struct has a VStack that contains two views: a Text view that displays the current points, and an ButtonView view that allows the user to increment the points.

class GameProgress: ObservableObject {
@Published var points = 0
}

struct ButtonView: View {
@ObservedObject var progress: GameProgress

var body: some View {
Button("Increase Points") {
progress.points += 1
}
}
}

struct ContentView: View {
@StateObject var progress = GameProgress()

var body: some View {
VStack {
Text("Your points are \(progress.points)")
ButtonView(progress: progress)
}
}
}

Here are some key takeaways from this code:

  • The @StateObject property wrapper is used to create an object that can be observed by other views.

  • The @Published property wrapper is used to mark a property in an ObservableObject class as being observable.

  • The @ObservedObject property wrapper is used to observe a property in an ObservableObject class from another view.

  • When the value of a property that is marked with the @Published property wrapper changes, the views that are observing the property will be updated automatically.

This is a simple example of how to use the @StateObject and @ObservedObject property wrappers to manage state in SwiftUI. In a more complex app, the GameProgress class would likely be responsible for managing more than just the points. It might also be responsible for fetching data from a server or interacting with other parts of the app.

Using @EnvironmentObject

final class MyTheme: ObservableObject {
@Published var mainColor: Color = .purple
}

struct ThemeApp: App {
@StateObject var myTheme = MyTheme()

var body: some Scene {
WindowGroup {
ThemesListView()
.environmentObject(myTheme) // Make the theme available through the environment.
}
}
}

And the ThemesListView struct will be,

struct ThemesListView: View {

@EnvironmentObject var myTheme: Theme

Text("Text Title")
.backgroundColor(myTheme.mainColor)

}

The code is for a SwiftUI app that uses an environment object to share a theme between views. The theme object is a MyTheme class that conforms to the ObservableObject protocol. This means that the theme object can be observed by other views.

The ThemeApp struct is the main entry point for the app. It creates a myTheme property that is an instance of the MyTheme class. The myTheme property is marked with the @StateObject property wrapper, which means that it is owned by the ThemeApp struct.

The ThemeApp struct also has a body property that returns a WindowGroup. The WindowGroup contains an ThemesListView view. The ThemesListView view is a view that displays a list of themes.

The ThemesListView view uses the environmentObject modifier to access the myTheme property. This modifier tells SwiftUI to look for the myTheme property in the environment of the ThemesListView view. If the myTheme property is not found in the environment, then a new instance of the MyTheme class will be created.

The ThemesListView view uses the myTheme.mainColor property to set the color of the list items. This means that the color of the list items will be updated automatically whenever the mainColor property of the myTheme object changes.

Using an environment object is a simple and elegant solution. We only have to create the theme object once, and it will be available to all child views automatically. This makes our code easier to read and maintain.

Conclusion

In this blog post, we have explored three different ways to manage state in SwiftUI. We have seen how to use the @State property wrapper to manage simple state, how to use the @StateObject and @ObservedObject property wrappers to manage complex state, and how to use environment objects to share state between views.

The best approach to use will depend on the specific needs of your app.

HOW TO INTEGRATE PUSH NOTIFICATIONS IN FLUTTER USING FIREBASE

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

Push notifications are a crucial component of modern mobile applications, allowing you to engage and re-engage users by sending timely updates and reminders.

In this blog post, we'll explore how to integrate push notifications in a Flutter app using Firebase Cloud Messaging (FCM). Firebase Cloud Messaging is a powerful and user-friendly platform that enables sending notifications to both Android and iOS devices.

Prerequisites

Before we begin, ensure that you have the following prerequisites in place:

  • Flutter Development Environment: Make sure you have Flutter and Dart installed on your system. If not, follow the official Flutter installation guide: Flutter Installation Guide

  • Firebase Project: Create a Firebase project if you haven't already. Visit the Firebase Console (https://console.firebase.google.com/) and set up a new project.

Step 1: Set Up Firebase Project

  • Go to the Firebase Console and select your project.

  • Click on "Project settings" and then navigate to the "Cloud Messaging" tab.

  • Here, you'll find your Server Key and Sender ID. These will be used later in your Flutter app to communicate with Firebase Cloud Messaging.

Step 2: Add Firebase Dependencies

In your Flutter project, open the pubspec.yaml file and add the necessary Firebase dependencies:

dependencies:
flutter:
sdk: flutter
firebase_core: ^1.12.0
firebase_messaging: ^11.1.0

After adding the dependencies, run flutter pub get to fetch them.

Step 3: Initialize Firebase

Open your main.dart file and initialize Firebase in the main function:

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

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}

Step 4: Request Notification Permissions

To receive push notifications, you need to request user permission. Add the following code to your main widget (usually MyApp):

import 'package:firebase_messaging/firebase_messaging.dart';

class MyApp extends StatelessWidget {
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

@override
Widget build(BuildContext context) {
// Request notification permissions
_firebaseMessaging.requestPermission();

return MaterialApp(
// ...
);
}
}

Step 5: Handle Notifications

Now let's handle incoming notifications. Add the following code to the same widget where you requested permissions:

class MyApp extends StatelessWidget {
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

@override
void initState() {
super.initState();

// Handle incoming messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
// Handle the message
print("Received message: ${message.notification?.title}");
});
}

@override
Widget build(BuildContext context) {
// ...
}
}

Step 6: Displaying Notifications

To display notifications when the app is in the background or terminated, you need to set up a background message handler. Add the following code to your main widget:

class MyApp extends StatelessWidget {
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

@override
void initState() {
super.initState();

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print("Received message: ${message.notification?.title}");
});

// Handle messages when the app is in the background or terminated
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
}

// Define the background message handler
Future&lt;void&gt; _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
print("Handling a background message: ${message.notification?.title}");
}

@override
Widget build(BuildContext context) {
// ...
}
}

Step 7: Sending Test Notifications

Now that your Flutter app is set up to receive notifications, let's test it by sending a test notification from the Firebase Console:

  • Go to the Firebase Console and select your project.

  • Navigate to the "Cloud Messaging" tab.

  • Click on the "New Notification" button.

  • Enter the notification details and target your app.

  • Click "Send Test Message."

Conclusion

Congratulations! You've successfully integrated push notifications in your Flutter app using Firebase Cloud Messaging. You've learned how to request notification permissions, handle incoming messages, and set up background message handling. This capability opens up a world of possibilities for engaging your users and providing timely updates.

Firebase Cloud Messaging provides even more features, such as sending notifications to specific topics, customizing notification appearance, and handling user interactions with notifications. Explore the Firebase Cloud Messaging documentation to learn more about these advanced features and take your app's notification experience to the next level.

Happy coding!