#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,
The user enters a number into a
Text Field
Then press the button
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,
Hold the user entered number
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
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