Flutter: Create Audio and Group Call App UI
In this blog, I am going to introduce Audio and Group Call UI. Along with learning the awesome Audio and Group Call UI implementation in Flutter, we will also learn how its coding workflows and structures work.
How to Create a New Future Project
First, we need to create a new Flutter project. For that, make sure that you've installed the Flutter SDK and other Flutter app development-related requirements.
If everything is properly set up, then in order to create a project we can simply run the following command in our desired local directory:
flutter create flutter-calling-app-ui
After we've set up the project, we can navigate inside the project directory and execute the following command in the terminal to run the project in either an available emulator or an actual device:
flutter run
First, we need to make some simple configurations to the default boilerplate code in the main.dart file. We'll remove some default code and add the simple MaterialApp
pointing to the remove Container
and Call AudioCall or GroupCall page for now:
import 'package:callingui/screens/audiocall/audiocall.dart';
import 'package:callingui/screens/groupcall/groupcall.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: AudioCall(),
);
}
}
Create a constants.dart and sizeConfig.dart file in inner lib folder
constants.dart
import 'package:flutter/material.dart';
const dBackgroundColor = Color(0xFF091C40);
const dSecondaryColor = Color(0xFF606060);
const dRedColor = Color(0xFFFF1E46);
sizeConfig.dart
import 'package:flutter/material.dart';
class SizeConfig{
static MediaQueryData _mediaQueryData = new MediaQueryData();
static double screenWidth = 0;
static double screenHeight = 0;
static double defaultSize = 0;
static Orientation? orientation;
void init(BuildContext context){
_mediaQueryData = MediaQuery.of(context);
screenWidth = _mediaQueryData.size.width;
screenHeight = _mediaQueryData.size.height;
orientation = _mediaQueryData.orientation;
}
}
double getProportionScreenHeight(double inputHeight){
double screenHeight = SizeConfig.screenHeight;
return (inputHeight / 896.0)* screenHeight;
}
double getProportionScreenWidth(double inputHeight){
double screenWidth = SizeConfig.screenWidth;
return (inputHeight / 414.0)* screenWidth;
}
class VerticalSapcing extends StatelessWidget {
final double of;
const VerticalSapcing({Key? key, this.of = 20 }):super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: getProportionScreenHeight(of),
);
}
}
class HorizontalSapcing extends StatelessWidget {
final double of;
const HorizontalSapcing({Key? key, this.of = 20 }):super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: getProportionScreenWidth(of),
);
}
}
Before Create body.dart,
dialuserpic.dart and
groupbody.dart
stateful widget file in widget folder, then inside of it with the following code:
body.dart
import 'package:callingui/constants.dart';
import 'package:callingui/sizeConfig.dart';
import 'package:flutter/material.dart';
class Body extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
Image.asset('assets/images/3.jpg', fit: BoxFit.cover),
DecoratedBox(
decoration: BoxDecoration(color: Colors.black.withOpacity(0.3))
),
Padding(
padding: const EdgeInsets.all(20.0),
child: SafeArea(child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Jimmy Ben", style: Theme.of(context).textTheme.headline3?.copyWith(color: Colors.white)),
VerticalSapcing(of: 10),
Text("Incoming 00.40".toUpperCase(), style: TextStyle(color: Colors.white60)),
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
height: getProportionScreenHeight(64),
width: getProportionScreenWidth(64),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.white),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
side: BorderSide(color: Colors.white)
)
)
),
onPressed: (){},
child: Icon(Icons.mic_none_outlined, color: Colors.black,),
),
),
SizedBox(
height: getProportionScreenHeight(64),
width: getProportionScreenWidth(64),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(dRedColor),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
side: BorderSide(color: dRedColor)
)
)
),
onPressed: (){},
child: Icon(Icons.call_end_sharp),
),
),
SizedBox(
height: getProportionScreenHeight(64),
width: getProportionScreenWidth(64),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.white),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
side: BorderSide(color: Colors.white)
)
)
),
onPressed: (){},
child: Icon(Icons.volume_up, color: Colors.black,),
),
)
],
)
],
)),
)
],
);
}
}
dialuserpic.dart
import 'package:callingui/sizeConfig.dart';
import 'package:flutter/material.dart';
class DialUserPic extends StatelessWidget {
final double size;
final String image;
const DialUserPic({Key? key, this.size = 1, required this.image }):super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(30/250 * size),
height: getProportionScreenHeight(size),
width: getProportionScreenWidth(size),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [Colors.white.withOpacity(0.02), Colors.white.withOpacity(0.05)],
stops: [0.5, 1]
)
),
child: CircleAvatar(
radius: 100,
backgroundImage: AssetImage(image),
)
);
}
}
groupbody.dart
import 'package:callingui/constants.dart';
import 'package:callingui/sizeConfig.dart';
import 'package:callingui/widgets/dialuserpic.dart';
import 'package:flutter/material.dart';
class GroupBody extends StatelessWidget {
List<Map<String, dynamic>> callingData = [
{
"isCalling":false,
"name":"User 1",
"image":"assets/images/1.jpg"
},
{
"isCalling":true,
"name":"User 2",
"image":"assets/images/2.jpg"
},
{
"isCalling":false,
"name":"User 3",
"image":"assets/images/3.jpg"
}
,{
"isCalling":false,
"name":"User 4",
"image":"assets/images/4.jpg"
}
];
@override
Widget build(BuildContext context) {
return GridView.builder(
padding: EdgeInsets.zero,
itemCount: callingData.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: 0.49,
crossAxisCount: 2,
// mainAxisSpacing: 20,
// crossAxisSpacing: 20
),
itemBuilder:(context, index) => callingData[index]["isCalling"]
? Container(
width: SizeConfig.screenWidth/2,
child: AspectRatio(
aspectRatio: 0.53,
child: Container(
color: dBackgroundColor,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DialUserPic(
size: 250,
image: callingData[index]["image"],
),
VerticalSapcing(of: 10),
Text(callingData[index]["name"], style: TextStyle(fontSize: 18, color: Colors.white)),
VerticalSapcing(of: 5),
Text("Calling...", style: TextStyle(color: Colors.white))
],
),
),
),
):
Image.asset(callingData[index]["image"], fit: BoxFit.cover,)
);
}
}
Before Create audiocall.dart
stateful widget file in screens folder like screens/audiocall/audiocall.dart, then inside of it with the following code:
import 'package:callingui/sizeConfig.dart';
import 'package:callingui/widgets/body.dart';
import 'package:flutter/material.dart';
class AudioCall extends StatelessWidget {
@override
Widget build(BuildContext context) {
SizeConfig().init(context);
return Scaffold(
body: Body(),
);
}
}
Before Create groupcall.dart
stateful widget file in screens folder like screens/groupcall/groupcall.dart, then inside of it with the following code:
import 'package:callingui/constants.dart';
import 'package:callingui/sizeConfig.dart';
import 'package:callingui/widgets/groupbody.dart';
import 'package:flutter/material.dart';
class GroupCall extends StatelessWidget {
const GroupCall({super.key});
@override
Widget build(BuildContext context) {
SizeConfig().init(context);
return Scaffold(
body: GroupBody(),
bottomNavigationBar: Container(
color: dBackgroundColor,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
height: getProportionScreenHeight(64),
width: getProportionScreenWidth(64),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(dRedColor),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
side: BorderSide(color: dRedColor)
)
)
),
onPressed: (){},
child: Icon(Icons.call_end_sharp),
),
),
SizedBox(
height: getProportionScreenHeight(64),
width: getProportionScreenWidth(64),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.white),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
// side: BorderSide(color: Colors.white)
)
)
),
onPressed: (){},
child: Icon(Icons.mic_none_outlined, color: Colors.black,),
),
),
SizedBox(
height: getProportionScreenHeight(64),
width: getProportionScreenWidth(64),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.white),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
// side: BorderSide(color: Colors.white)
)
)
),
onPressed: (){},
child: Icon(Icons.volume_up, color: Colors.black,),
),
),
SizedBox(
height: getProportionScreenHeight(64),
width: getProportionScreenWidth(64),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.white),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
// side: BorderSide(color: Colors.white)
)
)
),
onPressed: (){},
child: Icon(Icons.videocam_outlined, color: Colors.black,),
),
),
SizedBox(
height: getProportionScreenHeight(64),
width: getProportionScreenWidth(64),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.white),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(100),
)
)
),
onPressed: (){},
child: Icon(Icons.refresh_rounded, color: Colors.black,),
),
)
],
),
),
),
),
);
}
}
Output:
import 'package:callingui/screens/audiocall/audiocall.dart'; import 'package:callingui/screens/groupcall/groupcall.dart'; import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: AudioCall(), ); } }
import 'package:flutter/material.dart'; const dBackgroundColor = Color(0xFF091C40); const dSecondaryColor = Color(0xFF606060); const dRedColor = Color(0xFFFF1E46);
import 'package:flutter/material.dart'; class SizeConfig{ static MediaQueryData _mediaQueryData = new MediaQueryData(); static double screenWidth = 0; static double screenHeight = 0; static double defaultSize = 0; static Orientation? orientation; void init(BuildContext context){ _mediaQueryData = MediaQuery.of(context); screenWidth = _mediaQueryData.size.width; screenHeight = _mediaQueryData.size.height; orientation = _mediaQueryData.orientation; } } double getProportionScreenHeight(double inputHeight){ double screenHeight = SizeConfig.screenHeight; return (inputHeight / 896.0)* screenHeight; } double getProportionScreenWidth(double inputHeight){ double screenWidth = SizeConfig.screenWidth; return (inputHeight / 414.0)* screenWidth; } class VerticalSapcing extends StatelessWidget { final double of; const VerticalSapcing({Key? key, this.of = 20 }):super(key: key); @override Widget build(BuildContext context) { return SizedBox( height: getProportionScreenHeight(of), ); } } class HorizontalSapcing extends StatelessWidget { final double of; const HorizontalSapcing({Key? key, this.of = 20 }):super(key: key); @override Widget build(BuildContext context) { return SizedBox( height: getProportionScreenWidth(of), ); } }
Before Create body.dart,
dialuserpic.dart and
groupbody.dart
stateful widget file in widget folder, then inside of it with the following code:
body.dart
import 'package:callingui/constants.dart'; import 'package:callingui/sizeConfig.dart'; import 'package:flutter/material.dart'; class Body extends StatelessWidget { @override Widget build(BuildContext context) { return Stack( fit: StackFit.expand, children: [ Image.asset('assets/images/3.jpg', fit: BoxFit.cover), DecoratedBox( decoration: BoxDecoration(color: Colors.black.withOpacity(0.3)) ), Padding( padding: const EdgeInsets.all(20.0), child: SafeArea(child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("Jimmy Ben", style: Theme.of(context).textTheme.headline3?.copyWith(color: Colors.white)), VerticalSapcing(of: 10), Text("Incoming 00.40".toUpperCase(), style: TextStyle(color: Colors.white60)), Spacer(), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SizedBox( height: getProportionScreenHeight(64), width: getProportionScreenWidth(64), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.white), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), side: BorderSide(color: Colors.white) ) ) ), onPressed: (){}, child: Icon(Icons.mic_none_outlined, color: Colors.black,), ), ), SizedBox( height: getProportionScreenHeight(64), width: getProportionScreenWidth(64), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(dRedColor), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), side: BorderSide(color: dRedColor) ) ) ), onPressed: (){}, child: Icon(Icons.call_end_sharp), ), ), SizedBox( height: getProportionScreenHeight(64), width: getProportionScreenWidth(64), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.white), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), side: BorderSide(color: Colors.white) ) ) ), onPressed: (){}, child: Icon(Icons.volume_up, color: Colors.black,), ), ) ], ) ], )), ) ], ); } }
dialuserpic.dart
import 'package:callingui/sizeConfig.dart'; import 'package:flutter/material.dart'; class DialUserPic extends StatelessWidget { final double size; final String image; const DialUserPic({Key? key, this.size = 1, required this.image }):super(key: key); @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.all(30/250 * size), height: getProportionScreenHeight(size), width: getProportionScreenWidth(size), decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [Colors.white.withOpacity(0.02), Colors.white.withOpacity(0.05)], stops: [0.5, 1] ) ), child: CircleAvatar( radius: 100, backgroundImage: AssetImage(image), ) ); } }
groupbody.dart
import 'package:callingui/constants.dart'; import 'package:callingui/sizeConfig.dart'; import 'package:callingui/widgets/dialuserpic.dart'; import 'package:flutter/material.dart'; class GroupBody extends StatelessWidget { List<Map<String, dynamic>> callingData = [ { "isCalling":false, "name":"User 1", "image":"assets/images/1.jpg" }, { "isCalling":true, "name":"User 2", "image":"assets/images/2.jpg" }, { "isCalling":false, "name":"User 3", "image":"assets/images/3.jpg" } ,{ "isCalling":false, "name":"User 4", "image":"assets/images/4.jpg" } ]; @override Widget build(BuildContext context) { return GridView.builder( padding: EdgeInsets.zero, itemCount: callingData.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( childAspectRatio: 0.49, crossAxisCount: 2, // mainAxisSpacing: 20, // crossAxisSpacing: 20 ), itemBuilder:(context, index) => callingData[index]["isCalling"] ? Container( width: SizeConfig.screenWidth/2, child: AspectRatio( aspectRatio: 0.53, child: Container( color: dBackgroundColor, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ DialUserPic( size: 250, image: callingData[index]["image"], ), VerticalSapcing(of: 10), Text(callingData[index]["name"], style: TextStyle(fontSize: 18, color: Colors.white)), VerticalSapcing(of: 5), Text("Calling...", style: TextStyle(color: Colors.white)) ], ), ), ), ): Image.asset(callingData[index]["image"], fit: BoxFit.cover,) ); } }
Before Create audiocall.dart
stateful widget file in screens folder like screens/audiocall/audiocall.dart, then inside of it with the following code:
import 'package:callingui/sizeConfig.dart'; import 'package:callingui/widgets/body.dart'; import 'package:flutter/material.dart'; class AudioCall extends StatelessWidget { @override Widget build(BuildContext context) { SizeConfig().init(context); return Scaffold( body: Body(), ); } }
Before Create groupcall.dart
stateful widget file in screens folder like screens/groupcall/groupcall.dart, then inside of it with the following code:
import 'package:callingui/constants.dart'; import 'package:callingui/sizeConfig.dart'; import 'package:callingui/widgets/groupbody.dart'; import 'package:flutter/material.dart'; class GroupCall extends StatelessWidget { const GroupCall({super.key}); @override Widget build(BuildContext context) { SizeConfig().init(context); return Scaffold( body: GroupBody(), bottomNavigationBar: Container( color: dBackgroundColor, child: SafeArea( child: Padding( padding: const EdgeInsets.only(top: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ SizedBox( height: getProportionScreenHeight(64), width: getProportionScreenWidth(64), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(dRedColor), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), side: BorderSide(color: dRedColor) ) ) ), onPressed: (){}, child: Icon(Icons.call_end_sharp), ), ), SizedBox( height: getProportionScreenHeight(64), width: getProportionScreenWidth(64), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.white), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), // side: BorderSide(color: Colors.white) ) ) ), onPressed: (){}, child: Icon(Icons.mic_none_outlined, color: Colors.black,), ), ), SizedBox( height: getProportionScreenHeight(64), width: getProportionScreenWidth(64), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.white), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), // side: BorderSide(color: Colors.white) ) ) ), onPressed: (){}, child: Icon(Icons.volume_up, color: Colors.black,), ), ), SizedBox( height: getProportionScreenHeight(64), width: getProportionScreenWidth(64), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.white), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), // side: BorderSide(color: Colors.white) ) ) ), onPressed: (){}, child: Icon(Icons.videocam_outlined, color: Colors.black,), ), ), SizedBox( height: getProportionScreenHeight(64), width: getProportionScreenWidth(64), child: ElevatedButton( style: ButtonStyle( backgroundColor: MaterialStateProperty.all<Color>(Colors.white), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(100), ) ) ), onPressed: (){}, child: Icon(Icons.refresh_rounded, color: Colors.black,), ), ) ], ), ), ), ), ); } }