#1 - Whatsapp Opener Flutter App

Idea

Enter a phone number with the country code and press a button to open the Whatsapp app or web.

Techstack

  • Flutter

  • Packages

    • Flutter bloc

    • Freezed

  • VSCode

Let's code

Initial project setup

flutter create whatsapp_opener

Remove all the comments from pubspec.yaml and main.dart files.

Hint: If you are using VSCode, use the Remove Comments extension.

Update analysis options to maintain a clean flutter code

Folder structure

  • Application - BLOC notifiers

  • Presentation - Pages and Widgets

// lib/main.dart

import 'package:flutter/material.dart';

import 'presentation/app.dart';

void main() {
  runApp(const App());
}
// lib/presentation/app.dart

import 'package:flutter/material.dart';

import 'presentation/app.dart';

void main() {
  runApp(const App());
}
// lib/presentation/home_page.dart

import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return  Scaffold(
      appBar: AppBar(),
    );
  }
}

Now let's create the UI for the home page

  • A text field to enter a number

// lib/presentation/home_page.dart

import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Whatsapp Opener"),
        centerTitle: true,
      ),
      body: Container(
        padding: const EdgeInsets.symmetric(
          horizontal: 10,
          vertical: 5,
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            TextField(
              decoration: InputDecoration(labelText: 'Enter number'),
            ),
            ElevatedButton(
              onPressed: () {},
              child: const Text("Open Whatsapp"),
            )
          ],
        ),
      ),
    );
  }
}

Now let's create the logic

The main use case flow is,

  1. The user enters a number into a Text Field

  2. Then press the button

  3. Whatsapp opens with a chat for the entered number

To implement application logic I'm using the flutter_bloc package. Using that I'm going to,

  1. Hold the user entered number

  2. Use that number to open the chat when the user presses the button

Hint: Install the bloc extension to use flutter_bloc easily

To generate boilerplate codes and manage classes easily I'm using the freezed package with freezed_annotation and build_runner packages.

// pubspec.yaml

name: whatsapp_opener
description: Open whatsapp chat for any whatsapp user without saving the number

publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=2.19.0 <3.0.0'

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  flutter_bloc: ^8.1.1
  freezed_annotation: ^2.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0
  build_runner: ^2.3.3
  freezed: ^2.3.2

flutter:
  uses-material-design: true

Now right-click on the application folder and select the Cubit: New cubit option. It will give a prompt to enter the notifier name. I'm calling this AppActorNotifier.

After entering the name for the cubit, the bloc extension will create 2 files.

// lib/application/cubit/app_actor_notifier_cubit.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'app_actor_notifier_state.dart';
part 'app_actor_notifier_cubit.freezed.dart';

class AppActorNotifierCubit extends Cubit<AppActorNotifierState> {
  AppActorNotifierCubit() : super(AppActorNotifierState.initial());
}

// lib/application/cubit/app_actor_notifier_state.dart

part of 'app_actor_notifier_cubit.dart';

@freezed
class AppActorNotifierState with _$AppActorNotifierState {
  const factory AppActorNotifierState.initial() = _Initial;
}

Now you can see part 'app_actor_notifier_cubit.freezed.dart'; this line gives an error. Because that file doesn't exist yet. To generate that file run the build_runner.

flutter pub run build_runner watch --delete-conflicting-outputs

Note :

  • watch : Build_runner is going to keep watching for any changes and re-run itself to update the generated files

  • --delete-conflicting-outputs : Replace the old generated files with the newer ones

Now let's update the state class to hold the number input. Also, I'm renaming the cubit folder to app_actor_notifier.

// lib/application/app_actor_notifier/app_actor_notifier_state.dart

part of 'app_actor_notifier_cubit.dart';

@freezed
class AppActorNotifierState with _$AppActorNotifierState{
  const factory AppActorNotifierState({
    required String number,
  }) = _AppActorNotifierState;

  factory AppActorNotifierState.initial() => const AppActorNotifierState(
    number: '',
  );
}

Now let's create a method in the cubit to update the number value in the state when the user enters it.

// lib/application/app_actor_notifier/app_actor_notifier_cubit.dart

void onNumberChanged(String value) {
    emit(state.copyWith(number: value));
}

Note :

  • emit : emits the newest state

  • copyWith : Generated by freezed package to clone and create a new object with the updated values without changing other values in the object.

Now let's create the method to open the WhatsApp chat app with the number.

For this method, I'm using the url_launcher package.

// lib/application/app_actor_notifier/app_actor_notifier_cubit.dart

void openWhatsappChat() {
    if (state.number.isNotEmpty) {
      launchUrlString('https://wa.me/${state.number}');
    }
}

Note:

  • https://wa.me : Short url to open a whatsapp chat with a given number

  • state.number : Get number value from the state

Hint: After installing a plugin make sure to rebuild your app ( Not hot restart) . Platform-specific codes from plugins only get included in the build time. Otherwise, you'll get an Unimplemented Error.

Finally, we have,

// lib/application/app_actor_notifier/app_actor_notifier_cubit.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:url_launcher/url_launcher_string.dart';

part 'app_actor_notifier_state.dart';
part 'app_actor_notifier_cubit.freezed.dart';

class AppActorNotifierCubit extends Cubit<AppActorNotifierState> {
  AppActorNotifierCubit() : super(AppActorNotifierState.initial());

  void onNumberChanged(String value) {
    emit(state.copyWith(number: value));
  }

  void openWhatsappChat() {
    if (state.number.isNotEmpty) {
      launchUrlString('https://wa.me/${state.number}');
    }
  }
}

Now let's use the AppActorNotifier,

First, we have to provide an instance of the AppActorNotifierCubit to our app.

// lib/presentation/app.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:whatsapp_opener/application/app_actor_notifier/app_actor_notifier_cubit.dart';

import 'home_page.dart';

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => AppActorNotifierCubit(),
      child: MaterialApp(
        debugShowCheckedModeBanner: false,
        title: 'Whatsapp Opener',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const HomePage(),
      ),
    );
  }
}

Now we can bind our actor notifier to the home page.

// lib/presentation/home_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:whatsapp_opener/application/app_actor_notifier/app_actor_notifier_cubit.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Whatsapp Opener"),
        centerTitle: true,
      ),
      body: Container(
        padding: const EdgeInsets.symmetric(
          horizontal: 10,
          vertical: 5,
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            TextField(
              decoration: const InputDecoration(labelText: 'Enter number'),
              onChanged: (value) {
                if (value.trim().isNotEmpty) {
                  context.read<AppActorNotifierCubit>().onNumberChanged(value);
                }
              },
            ),
            ElevatedButton(
              onPressed: () {
                context.read<AppActorNotifierCubit>().openWhatsappChat();
              },
              child: const Text("Open Whatsapp"),
            )
          ],
        ),
      ),
    );
  }
}

Note :

  • context.read : Equal to BlocProvider.of(context) which looks up the closest ancestor instance of the specified type. Use to call events. Learn more.

Validations

  • If the user enters an invalid number app should show an error

  • Submit button should stay disabled until the user enters a valid number

// lib/application/app_actor_notifier/app_actor_notifier_state.dart

part of 'app_actor_notifier_cubit.dart';

@freezed
class AppActorNotifierState with _$AppActorNotifierState{
  const factory AppActorNotifierState({
    required String number,
    required String error,
  }) = _AppActorNotifierState;

  factory AppActorNotifierState.initial() => const AppActorNotifierState(
    number: '',
    error: '',
  );
}
// lib/application/app_actor_notifier/app_actor_notifier_cubit.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:url_launcher/url_launcher_string.dart';

part 'app_actor_notifier_state.dart';
part 'app_actor_notifier_cubit.freezed.dart';

class AppActorNotifierCubit extends Cubit<AppActorNotifierState> {
  AppActorNotifierCubit() : super(AppActorNotifierState.initial());

  void onNumberChanged(String value) {
    if (value.contains(RegExp(r"^\+[\d\s]{2,}$"))) {
      final valueWithoutSpaces = value.replaceAll(RegExp(r"\s+"), "");

      emit(state.copyWith(
        number: valueWithoutSpaces,
        error: '',
      ));
    } else {
      emit(state.copyWith(
        error: 'Please enter a valid number',
      ));
    }
  }

  void openWhatsappChat() {
    if (state.number.isNotEmpty) {
      launchUrlString('https://wa.me/${state.number}');
    }
  }
}
// lib/presentation/home_page.dart

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

import '../application/app_actor_notifier/app_actor_notifier_cubit.dart';

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Whatsapp Opener"),
        centerTitle: true,
      ),
      body: Container(
        padding: const EdgeInsets.symmetric(
          horizontal: 10,
          vertical: 5,
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            TextField(
              decoration: InputDecoration(
                labelText: 'Enter number',
                errorText: context
                    .select((AppActorNotifierCubit cubit) => cubit.state.error),
              ),
              onChanged: (value) {
                if (value.trim().isNotEmpty) {
                  context.read<AppActorNotifierCubit>().onNumberChanged(value);
                }
              },
            ),
            ElevatedButton(
              onPressed: context.select((AppActorNotifierCubit cubit) =>
                      cubit.state.error.isEmpty)
                  ? () {
                      context.read<AppActorNotifierCubit>().openWhatsappChat();
                    }
                  : null,
              child: const Text("Open Whatsapp"),
            )
          ],
        ),
      ),
    );
  }
}

Note:

  • context.watch : Provides the closest ancestor instance of the specified type and listens to changes on the instance. Equal to BlocProvider.of(context, listen: true). Learn more.

  • context.select : Same as context.watch, but allows you listen for changes in a smaller part of a state. Learn more.

Conclusion

Now we can open a chat with any Whatsapp user without saving the contact in the device.

GitHub: https://github.com/NirmalAriyathilake/blog_whatsapp_opener

“Good code is its own best documentation. As you're about to add a comment, ask yourself, 'How can I improve the code so that this comment isn't needed?' Improve the code and then document it to make it even clearer.” - Steve McConnell

0
Subscribe to my newsletter

Read articles from Nirmal Ariyathilake directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nirmal Ariyathilake
Nirmal Ariyathilake

Software Engineer | Flutter Fanatice