diff --git a/.gitignore b/.gitignore index 07488ba61a..3cf40fa3aa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ .buildlog/ .history .svn/ +*.lock + # IntelliJ related *.iml @@ -61,6 +63,10 @@ **/ios/Flutter/flutter_assets/ **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* +*.sh + +# Firebase +*.json # Exceptions to above rules. !**/ios/**/default.mode1v3 diff --git a/README.md b/README.md index 11fe08c30f..e2a569954e 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ We’re going to build a modern messaging app where users can sign up and log in - How to incorporate Firebase into your Flutter projects. - How to use Firebase authentication to register and sign in users. - How to create beautiful animations using the Flutter Hero widget. -- How to create custom aniamtions using Flutter's animation controller. +- How to create custom animations using Flutter's animation controller. - Learn all about mixins and how they differ from superclasses. - Learn about Streams and how they work. - Learn to use ListViews to build scrolling views. diff --git a/android/app/build.gradle b/android/app/build.gradle index e8d2b49ddc..3770f3e851 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,6 +24,7 @@ if (flutterVersionName == null) { apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +apply plugin: 'com.google.gms.google-services' android { compileSdkVersion 29 @@ -34,8 +35,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "co.appbrewery.flash_chat" - minSdkVersion 16 + applicationId "com.nnq.flash_chat" + minSdkVersion 19 targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -57,4 +58,5 @@ flutter { dependencies { implementation 'androidx.multidex:multidex:2.0.0' + implementation platform('com.google.firebase:firebase-bom:29.0.3') } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 29713595af..329cc0efeb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> - + + android:name="io.flutter.embedding.android.SplashScreenDrawable" + android:resource="@drawable/launch_background" /> + + + + diff --git a/android/app/src/main/java/co/appbrewery/flash_chat/MainActivity.java b/android/app/src/main/java/co/appbrewery/flash_chat/MainActivity.java index b68d4927dc..a75b890059 100644 --- a/android/app/src/main/java/co/appbrewery/flash_chat/MainActivity.java +++ b/android/app/src/main/java/co/appbrewery/flash_chat/MainActivity.java @@ -1,13 +1,9 @@ package co.appbrewery.flash_chat; -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; + +import io.flutter.embedding.android.FlutterActivity; + public class MainActivity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } + } diff --git a/android/build.gradle b/android/build.gradle index 6de372893d..12a537a680 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,18 +1,19 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' + classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.google.gms:google-services:4.3.10' } } allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 63ab3ae08f..bc6a58afdd 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/lib/components/rounded_button.dart b/lib/components/rounded_button.dart new file mode 100644 index 0000000000..6c57dc1fa4 --- /dev/null +++ b/lib/components/rounded_button.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class RoundedButton extends StatelessWidget { + const RoundedButton({this.colour, this.title, @required this.onPressed}); + + final Color colour; + final String title; + final Function onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Material( + elevation: 5.0, + color: colour, + borderRadius: BorderRadius.circular(30.0), + child: MaterialButton( + onPressed: onPressed, + minWidth: 200.0, + height: 42.0, + child: Text(title), + ), + ), + ); + } +} diff --git a/lib/constants.dart b/lib/constants.dart index 25e5f5c22f..de443a9457 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -17,3 +17,20 @@ const kMessageContainerDecoration = BoxDecoration( top: BorderSide(color: Colors.lightBlueAccent, width: 2.0), ), ); + +const kTextFieldDecoration = InputDecoration( + hintText: '', + hintStyle: TextStyle(color: Colors.black45), + contentPadding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(32.0)), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.lightBlueAccent, width: 1.0), + borderRadius: BorderRadius.all(Radius.circular(32.0)), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.lightBlueAccent, width: 2.0), + borderRadius: BorderRadius.all(Radius.circular(32.0)), + ), +); diff --git a/lib/main.dart b/lib/main.dart index 6ea23d095c..4164a00e32 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,8 +3,13 @@ import 'package:flash_chat/screens/welcome_screen.dart'; import 'package:flash_chat/screens/login_screen.dart'; import 'package:flash_chat/screens/registration_screen.dart'; import 'package:flash_chat/screens/chat_screen.dart'; +import 'package:firebase_core/firebase_core.dart'; -void main() => runApp(FlashChat()); +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + runApp(FlashChat()); +} class FlashChat extends StatelessWidget { @override @@ -12,10 +17,16 @@ class FlashChat extends StatelessWidget { return MaterialApp( theme: ThemeData.dark().copyWith( textTheme: TextTheme( - body1: TextStyle(color: Colors.black54), + bodyText1: TextStyle(color: Colors.black54), ), ), - home: WelcomeScreen(), + initialRoute: WelcomeScreen.id, + routes: { + WelcomeScreen.id: (context) => WelcomeScreen(), + LoginScreen.id: (context) => LoginScreen(), + RegistrationScreen.id: (context) => RegistrationScreen(), + ChatScreen.id: (context) => ChatScreen(), + }, ); } } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 42d8b67b9b..d0e4e79439 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -1,12 +1,38 @@ import 'package:flutter/material.dart'; import 'package:flash_chat/constants.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; class ChatScreen extends StatefulWidget { + static const String id = 'chat_screen'; + @override _ChatScreenState createState() => _ChatScreenState(); } class _ChatScreenState extends State { + final _auth = FirebaseAuth.instance; + final _firestore = FirebaseFirestore.instance; + User loggedInUser; + String messageText; + + @override + void initState() { + super.initState(); + getCurrentUser(); + } + + void getCurrentUser() { + try { + final user = _auth.currentUser; + if (user != null) { + loggedInUser = user; + } + } catch (e) { + print(e); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -14,10 +40,13 @@ class _ChatScreenState extends State { leading: null, actions: [ IconButton( - icon: Icon(Icons.close), - onPressed: () { - //Implement logout functionality - }), + icon: Icon(Icons.close), + onPressed: () { + //Implement logout functionality + _auth.signOut(); + Navigator.pop(context); + }, + ), ], title: Text('⚡️Chat'), backgroundColor: Colors.lightBlueAccent, @@ -35,14 +64,17 @@ class _ChatScreenState extends State { Expanded( child: TextField( onChanged: (value) { - //Do something with the user input. + //save user input to a variable. + messageText = value; }, decoration: kMessageTextFieldDecoration, ), ), - FlatButton( + TextButton( onPressed: () { - //Implement send functionality. + //save message to database. + _firestore.collection('messages').add( + {'sender': loggedInUser.email, 'text': messageText}); }, child: Text( 'Send', diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 852e116a19..be9c40a4a1 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -1,99 +1,95 @@ +import 'package:flash_chat/constants.dart'; +import 'package:flash_chat/screens/chat_screen.dart'; import 'package:flutter/material.dart'; +import 'package:flash_chat/components/rounded_button.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:modal_progress_hud/modal_progress_hud.dart'; class LoginScreen extends StatefulWidget { + static const String id = 'login_screen'; + @override _LoginScreenState createState() => _LoginScreenState(); } class _LoginScreenState extends State { + bool showSpinner = false; + final _auth = FirebaseAuth.instance; + String userEmail; + String userPassword; + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - body: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - height: 200.0, - child: Image.asset('images/logo.png'), - ), - SizedBox( - height: 48.0, - ), - TextField( - onChanged: (value) { - //Do something with the user input. - }, - decoration: InputDecoration( - hintText: 'Enter your email', - contentPadding: - EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(32.0)), - ), - enabledBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.lightBlueAccent, width: 1.0), - borderRadius: BorderRadius.all(Radius.circular(32.0)), - ), - focusedBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.lightBlueAccent, width: 2.0), - borderRadius: BorderRadius.all(Radius.circular(32.0)), + body: ModalProgressHUD( + inAsyncCall: showSpinner, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Hero( + tag: 'logo', + child: Container( + height: 200.0, + child: Image.asset('images/logo.png'), ), ), - ), - SizedBox( - height: 8.0, - ), - TextField( - onChanged: (value) { - //Do something with the user input. - }, - decoration: InputDecoration( - hintText: 'Enter your password.', - contentPadding: - EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(32.0)), - ), - enabledBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.lightBlueAccent, width: 1.0), - borderRadius: BorderRadius.all(Radius.circular(32.0)), - ), - focusedBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.lightBlueAccent, width: 2.0), - borderRadius: BorderRadius.all(Radius.circular(32.0)), - ), + SizedBox( + height: 48.0, ), - ), - SizedBox( - height: 24.0, - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Material( - color: Colors.lightBlueAccent, - borderRadius: BorderRadius.all(Radius.circular(30.0)), - elevation: 5.0, - child: MaterialButton( - onPressed: () { - //Implement login functionality. - }, - minWidth: 200.0, - height: 42.0, - child: Text( - 'Log In', - ), - ), + TextField( + keyboardType: TextInputType.emailAddress, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.black), + onChanged: (value) { + userEmail = value; + }, + decoration: + kTextFieldDecoration.copyWith(hintText: 'Enter your email'), + ), + SizedBox( + height: 8.0, + ), + TextField( + obscureText: true, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.black), + onChanged: (value) { + userPassword = value; + }, + decoration: kTextFieldDecoration.copyWith( + hintText: 'Enter your password'), + ), + SizedBox( + height: 24.0, + ), + RoundedButton( + title: 'Log In', + colour: Colors.lightBlueAccent, + onPressed: () async { + setState(() { + showSpinner = true; + }); + try { + final registeredUser = + await _auth.signInWithEmailAndPassword( + email: userEmail, password: userPassword); + if (registeredUser != null) { + Navigator.pushNamed(context, ChatScreen.id); + } + setState(() { + showSpinner = false; + }); + } catch (e) { + print(e); + } + }, ), - ), - ], + ], + ), ), ), ); diff --git a/lib/screens/registration_screen.dart b/lib/screens/registration_screen.dart index bbc0d5195e..8d2b72ec6d 100644 --- a/lib/screens/registration_screen.dart +++ b/lib/screens/registration_screen.dart @@ -1,96 +1,97 @@ +import 'package:flash_chat/screens/chat_screen.dart'; import 'package:flutter/material.dart'; +import 'package:flash_chat/components/rounded_button.dart'; +import 'package:flash_chat/constants.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:modal_progress_hud/modal_progress_hud.dart'; class RegistrationScreen extends StatefulWidget { + static const String id = 'registration_screen'; + @override _RegistrationScreenState createState() => _RegistrationScreenState(); } class _RegistrationScreenState extends State { + bool showSpinner = false; + String email; + String password; + + final _auth = FirebaseAuth.instance; + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - body: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - height: 200.0, - child: Image.asset('images/logo.png'), - ), - SizedBox( - height: 48.0, - ), - TextField( - onChanged: (value) { - //Do something with the user input. - }, - decoration: InputDecoration( - hintText: 'Enter your email', - contentPadding: - EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(32.0)), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.blueAccent, width: 1.0), - borderRadius: BorderRadius.all(Radius.circular(32.0)), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.blueAccent, width: 2.0), - borderRadius: BorderRadius.all(Radius.circular(32.0)), + body: ModalProgressHUD( + inAsyncCall: showSpinner, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Hero( + tag: 'logo', + child: Container( + height: 200.0, + child: Image.asset('images/logo.png'), ), ), - ), - SizedBox( - height: 8.0, - ), - TextField( - onChanged: (value) { - //Do something with the user input. - }, - decoration: InputDecoration( - hintText: 'Enter your password', - contentPadding: - EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0), - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(32.0)), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.blueAccent, width: 1.0), - borderRadius: BorderRadius.all(Radius.circular(32.0)), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.blueAccent, width: 2.0), - borderRadius: BorderRadius.all(Radius.circular(32.0)), - ), + SizedBox( + height: 48.0, ), - ), - SizedBox( - height: 24.0, - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Material( - color: Colors.blueAccent, - borderRadius: BorderRadius.all(Radius.circular(30.0)), - elevation: 5.0, - child: MaterialButton( - onPressed: () { - //Implement registration functionality. - }, - minWidth: 200.0, - height: 42.0, - child: Text( - 'Register', - style: TextStyle(color: Colors.white), - ), - ), + TextField( + keyboardType: TextInputType.emailAddress, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.black), + onChanged: (value) { + email = value; + }, + decoration: + kTextFieldDecoration.copyWith(hintText: 'Enter your email'), + ), + SizedBox( + height: 8.0, + ), + TextField( + obscureText: true, + textAlign: TextAlign.center, + style: TextStyle(color: Colors.black), + onChanged: (value) { + password = value; + }, + decoration: kTextFieldDecoration.copyWith( + hintText: 'Enter your password'), + ), + SizedBox( + height: 24.0, ), - ), - ], + RoundedButton( + title: 'Register', + colour: Colors.blueAccent, + onPressed: () async { + setState(() { + showSpinner = true; + }); + // create new user + try { + final newUser = + await _auth.createUserWithEmailAndPassword( + email: email, password: password); + // if user already exists, navigate to chat screen + if (newUser != null) { + Navigator.pushNamed(context, ChatScreen.id); + } + setState(() { + showSpinner = false; + }); + } catch (e) { + print(e); + } + }), + ], + ), ), ), ); diff --git a/lib/screens/welcome_screen.dart b/lib/screens/welcome_screen.dart index 6270ccf476..d81f33276d 100644 --- a/lib/screens/welcome_screen.dart +++ b/lib/screens/welcome_screen.dart @@ -1,15 +1,50 @@ +import 'package:flash_chat/screens/login_screen.dart'; +import 'package:flash_chat/screens/registration_screen.dart'; import 'package:flutter/material.dart'; +import 'package:animated_text_kit/animated_text_kit.dart'; +import 'package:flash_chat/components/rounded_button.dart'; class WelcomeScreen extends StatefulWidget { + static const String id = 'welcome_screen'; @override _WelcomeScreenState createState() => _WelcomeScreenState(); } -class _WelcomeScreenState extends State { +class _WelcomeScreenState extends State + with SingleTickerProviderStateMixin { + AnimationController controller; + Animation animation; + + @override + void initState() { + super.initState(); + + controller = AnimationController( + vsync: this, + duration: Duration(seconds: 1), + ); + + animation = ColorTween(begin: Colors.blueGrey, end: Colors.white) + .animate(controller); + + controller.forward(); + + controller.addListener(() { + setState(() {}); + print(animation.value); + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, + backgroundColor: animation.value, body: Padding( padding: EdgeInsets.symmetric(horizontal: 24.0), child: Column( @@ -18,58 +53,44 @@ class _WelcomeScreenState extends State { children: [ Row( children: [ - Container( - child: Image.asset('images/logo.png'), - height: 60.0, - ), - Text( - 'Flash Chat', - style: TextStyle( - fontSize: 45.0, - fontWeight: FontWeight.w900, + Hero( + tag: 'logo', + child: Container( + child: Image.asset('images/logo.png'), + height: 60.0, ), ), + AnimatedTextKit( + animatedTexts: [ + TypewriterAnimatedText( + 'Flash Chat', + textStyle: TextStyle( + fontSize: 45.0, + fontWeight: FontWeight.w900, + color: Colors.black54, + ), + ), + ], + ), ], ), SizedBox( height: 48.0, ), - Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Material( - elevation: 5.0, - color: Colors.lightBlueAccent, - borderRadius: BorderRadius.circular(30.0), - child: MaterialButton( - onPressed: () { - //Go to login screen. - }, - minWidth: 200.0, - height: 42.0, - child: Text( - 'Log In', - ), - ), - ), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Material( - color: Colors.blueAccent, - borderRadius: BorderRadius.circular(30.0), - elevation: 5.0, - child: MaterialButton( - onPressed: () { - //Go to registration screen. - }, - minWidth: 200.0, - height: 42.0, - child: Text( - 'Register', - ), - ), - ), + RoundedButton( + colour: Colors.lightBlueAccent, + title: 'Log in', + onPressed: () { + Navigator.pushNamed(context, LoginScreen.id); + }, ), + RoundedButton( + colour: Colors.blueAccent, + title: 'Register', + onPressed: () { + Navigator.pushNamed(context, RegistrationScreen.id); + }, + ) ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index e6d5ae3d17..f25ea3e625 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,13 @@ dependencies: flutter: sdk: flutter - cupertino_icons: ^0.1.2 + cupertino_icons: ^1.0.4 + animated_text_kit: ^4.2.1 + firebase_core: ^1.11.0 + firebase_auth: ^3.3.5 + cloud_firestore: ^3.1.6 + modal_progress_hud: ^0.1.3 + dev_dependencies: flutter_test: