thumb - Instagram Clone Clean Architecture

Instagram Clone Clean Architecture – Part 7

Table of Contents
Instagram Clone Clean Architecture

(Instagram Clone Clean Architecture) In the last article we’ve put some effort in our Domain Layer for User, we created a contract class which for now only contains the User methods and we also have created UserEntity and understand some concepts, and with this we created separate Use Cases for each of the method in our Repository. Now let’s go for their implementation.

First things First, you have to setup your Firebase for your project because we’re going to use it in a while and that’s one of the important step to do.

Firebase Setup - Instagram Clone Clean Architecture

Go to > browser > search for firebase console open the first tab or the tab named Firebase Console, you’ll see this:

add project Instagram Clone Clean Architecture

Go for Add project, Name your project and Enable Firebase Analytics and select default account go Next and Finish up project. After this you’ll see your Firebase project is created and is ready to link it with your Flutter app:

add app Instagram Clone Clean Architecture

Go for this + Add app now and forget the name and the already created app in the left side.

You’ll see this after hitting + Add app.

select Instagram Clone Clean Architecture

Step 1:

Go for Android now and then another window will appear in front of you.

setup - Instagram Clone Clean Architecture

Now go to your flutter project in Android Studio or your Editor and search for the android directory mostly it will be located upside from the lib directory. Open it and Go to > android > app and open the build.grade.

  • Note : We’ve two build.grade files in our project on is App-Level other is Project-Level.

The one located in app directory is app-level so click on this and scroll a bit you’ll see you applicationId in defaultConfig{ } like the image below.

				
					       defaultConfig {
            // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
            applicationId "com.example.instagram_clone_app"
            minSdkVersion 19
            multiDexEnabled true
            targetSdkVersion flutter.targetSdkVersion
            versionCode flutterVersionCode.toInteger()
            versionName flutterVersionName
        }
				
			

Copy the applicationId and go to your browser and paste it in the first field and then If you want to give nick name to your app its optional and forget the SHA for now we’ll look it up later.

 

Hit the Register App button and Go forward.

 

Step 2:

You’ll see this window in front of you.

Download the google-servies.json file and move it from Downloads to Your Project > android > app as shown in the image above.

 

This .json file has the details for your android app you’ve created in a while you can check it by opening it.

 

Go Next.

Step 3:

In this step you’ll see some code that is essential for your android setup.

				
					classpath 'com.google.gms:google-services:4.3.13'
				
			

Copy this classpath and Go to > android > and as you’ve seen in the Note above we’ve two build.gradle files one is App-Level other is Project-Level so you need to paste this class path in your Project-Level build.gradle inside the dependencies{ }.

				
					    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.google.gms:google-services:4.3.13'
    }
				
			

That was it for Project-Level build.gradle now copy the below code:

				
					apply plugin: 'com.google.gms.google-services'
				
			

And go to the App-Level build.gradle file inside the android > app > build.gradle and paste it with other plugins.

				
					apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
apply plugin: 'com.google.gms.google-services'

				
			

You’re almost done with your setup.

Go for the Next again and Continue to console.

 

Now Initialize Firebase in your main.dart and RUN your app.

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

				
			

For Firebase.initializeApp() you need to have firebase_core package added your pubspec.yaml file.

Congrats 🎉 you’ve successfully set up firebase for your application.

Data Layer (Repository Implementation)

Now in the Instagram Clone Clean Architecture let’s go for the Data Layer Go to > data > repository > and create the dart file firebase_repository_impl.dart

There will goes the implementation of our FirebaseRepository (contract).

 

Create a class and name it to FirebaseRepostioryImpl and implements it with FirebaseRepository class.

				
					class FirebaseRepositoryImpl implements FirebaseRepository {

}

				
			

You’ll see compile time error in class name after implementing the FirebaseRepository with FirebaseRepositoryImpl.

Press Alt + Enter key and a little window will appear of 9 missing overrides select this and hit Enter.

You’ll see the result something like the code below:

				
					class FirebaseRepositoryImpl implements FirebaseRepository {
  @override
  Future<void> createUser(UserEntity user) {
    // TODO: implement createUser
    throw UnimplementedError();
  }

  @override
  Future<String> getCurrentUid() {
    // TODO: implement getCurrentUid
    throw UnimplementedError();
  }

  @override
  Stream<List<UserEntity>> getSingleUser(String uid) {
    // TODO: implement getSingleUser
    throw UnimplementedError();
  } 
  . . . . 
}

				
			

There will be all the methods which we had in FirebaseRepostiory (contract) class, but all of them will appear to be throwing UnimplementedError() for now.

 

Now it’s obvious this FirebaseRepositoryImpl will take the data from some kind of data source (hence we have directory in our Data Layer named data_sources).

So Inside the data_sources directory create a new directory and name it to remote_data_sources and inside this create a dart file remote_data_source.dart.

Now there will be the class which will be similar to the FirebaseRepository (contract) class only its name will be different look at the code below.

remote_data_source.dart

				
					import 'package:instagram_clone_app/features/domain/entities/user/user_entity.dart';

abstract class FirebaseRemoteDataSource {
  // Credential
  Future<void> signInUser(UserEntity user);
  Future<void> signUpUser(UserEntity user);
  Future<bool> isSignIn();
  Future<void> signOut();

  // User
  Stream<List<UserEntity>> getUsers(UserEntity user);
  Stream<List<UserEntity>> getSingleUser(String uid);
  Future<String> getCurrentUid();
  Future<void> createUser(UserEntity user);
  Future<void> updateUser(UserEntity user);
}

				
			

It’s same but name is FirebaseRemoteDataSource.

As our Repository (contract) in the Domain Layer has RepositoryImpl in the Data Layer, So this FirebaseRemoteDataSource is also a contract and its implementation will be inside the same directory so create a file in the data > data_sources > remote_data_sources > remote_data_source_impl and keep it empty for now we will come to it later.

data sources Instagram Clone Clean Architecture

Now it’s time to implement our FirebaseRepository. Go to > data > repository > and open firebase_repository.dart file.

 

Take the instance of the FirebaseRemoteDataSource class in the FirebaseRepositoryImpl class and also pass it in the constructor and make it required.

				
					final FirebaseRemoteDataSource remoteDataSource;

FirebaseRepositoryImpl({required this.remoteDataSource});

				
			

Now remote the UnImplemented error from all of the methods and call the particular methods for all of them like the code below:

				
					@override
Future<void> createUser(UserEntity user) async => remoteDataSource.createUser(user);

@override
Future<String> getCurrentUid() async => remoteDataSource.getCurrentUid();

				
			

These are two override methods I removed the { } body and make them arrow function put async as this method is Future and call the methods from the remoteDataSource instance like the code above.

Note: We also have Stream methods that are getSingleUser and getUsers so avoid to put async there otherwise this will throw compile time error.

Models

models Instagram Clone Clean Architecture

These models will going to store in our Cloud Firestore database. You may have a question why we have models and entities we could have only models or only entities.

Answer: We need to keep Domain Layer completely independent it’s not going to deal with any kind of third party, that’s the reason we created entity there (Domain Layer) and models in the Data Layer.

With this, models gonna have some conversions like in our case fromSnapshot() and toJson().

 

Our user model will going to look like the code below:

user_model.dart

Instagram Clone Clean Architecture

				
					import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:instagram_clone_app/features/domain/entities/user/user_entity.dart';

class UserModel extends UserEntity {
  final String? uid;
  final String? username;
  final String? name;
  final String? bio;
  final String? website;
  final String? email;
  final String? profileUrl;
  final List? followers;
  final List? following;
  final num? totalFollowers;
  final num? totalFollowing;
  final num? totalPosts;

  UserModel({
    this.uid,
    this.username,
    this.name,
    this.bio,
    this.website,
    this.email,
    this.profileUrl,
    this.followers,
    this.following,
    this.totalFollowers,
    this.totalFollowing,
    this.totalPosts,
  }) : super(
    uid: uid,
    totalFollowing: totalFollowing,
    followers: followers,
    totalFollowers: totalFollowers,
    username: username,
    profileUrl: profileUrl,
    website: website,
    following: following,
    bio: bio,
    name: name,
    email: email,
    totalPosts: totalPosts,
  );

  factory UserModel.fromSnapshot(DocumentSnapshot snap) {
    var snapshot = snap.data() as Map<String, dynamic>;

    return UserModel(
      email: snapshot['email'],
      name: snapshot['name'],
      bio: snapshot['bio'],
      username: snapshot['username'],
      totalFollowers: snapshot['totalFollowers'],
      totalFollowing: snapshot['totalFollowing'],
      totalPosts: snapshot['totalPosts'],
      uid: snapshot['uid'],
      website: snapshot['website'],
      profileUrl: snapshot['profileUrl'],
      followers: List.from(snap.get("followers")),
      following: List.from(snap.get("following")),
    );
  }

  Map<String, dynamic> toJson() => {
    "uid": uid,
    "email": email,
    "name": name,
    "username": username,
    "totalFollowers": totalFollowers,
    "totalFollowing": totalFollowing,
    "totalPosts": totalPosts,
    "website": website,
    "profileUrl": profileUrl,
    "followers": followers,
    "following": following,
  };
}

				
			

Notice:  As mentioned (before) we don’t have password and otherUid in the user model.

Data Layer (Remote Data Source Impl)

remote data source Instagram Clone Clean Architecture

We have implemented our FirebaseRepository and also created the model for our user, with this we also create an abstract class which is FirebaseRemoteDataSource from which we get the data while implementing the FirebaseRepository is and not implemented itself.

Go to data > data_sources > remote_data_sources > open remote_data_source_impl dart file.

 

As we did for FirebaseRepositoryImpl we implement the abstract class from the Impl class the same will goes for the FirebaseRemoteDataSource.

 

Create a class and implement it with FirebaseRemoteDataSource, again you’ll see the compile time error in the class press Alt + Enter key and select 9 missing overrides and hit Enter

There will come the unimplemented methods as we’ve seen in case of FirebaseRepository Implementation.

 

Now implement these methods :

remote_data_source_impl.dart

Instagram Clone Clean Architecture

				
					import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:instagram_clone_app/consts.dart';
import 'package:instagram_clone_app/features/data/data_sources/remote_data_source/remote_data_source.dart';
import 'package:instagram_clone_app/features/data/models/user/user_model.dart';
import 'package:instagram_clone_app/features/domain/entities/user/user_entity.dart';


class FirebaseRemoteDataSourceImpl implements FirebaseRemoteDataSource {
  final FirebaseFirestore firebaseFirestore;
  final FirebaseAuth firebaseAuth;

  FirebaseRemoteDataSourceImpl({required this.firebaseFirestore, required this.firebaseAuth});

  @override
  Future<void> createUser(UserEntity user) async {
    final userCollection = firebaseFirestore.collection(FirebaseConst.users);

    final uid = await getCurrentUid();

    userCollection.doc(uid).get().then((userDoc) {
      final newUser = UserModel(
          uid: uid,
          name: user.name,
          email: user.email,
          bio: user.bio,
          following: user.following,
          website: user.website,
          profileUrl: user.profileUrl,
          username: user.username,
          totalFollowers: user.totalFollowers,
          followers: user.followers,
          totalFollowing: user.totalFollowing,
          totalPosts: user.totalPosts
      ).toJson();

      if (!userDoc.exists) {
        userCollection.doc(uid).set(newUser);
      } else {
        userCollection.doc(uid).update(newUser);
      }
    }).catchError((error) {
      toast("Some error occur");
    });
  }

  @override
  Future<String> getCurrentUid() async => firebaseAuth.currentUser!.uid;


  @override
  Stream<List<UserEntity>> getSingleUser(String uid) {
    final userCollection = firebaseFirestore.collection(FirebaseConst.users).where("uid", isEqualTo: uid).limit(1);
    return userCollection.snapshots().map((querySnapshot) => querySnapshot.docs.map((e) => UserModel.fromSnapshot(e)).toList());
  }

  @override
  Stream<List<UserEntity>> getUsers(UserEntity user) {
    final userCollection = firebaseFirestore.collection(FirebaseConst.users);
    return userCollection.snapshots().map((querySnapshot) => querySnapshot.docs.map((e) => UserModel.fromSnapshot(e)).toList());
  }

  @override
  Future<bool> isSignIn() async => firebaseAuth.currentUser?.uid != null;

  @override
  Future<void> signInUser(UserEntity user)async {
    try {
      if (user.email!.isNotEmpty || user.password!.isNotEmpty) {
        await firebaseAuth.signInWithEmailAndPassword(email: user.email!, password: user.password!);
      } else {
        print("fields cannot be empty");
      }
    } on FirebaseAuthException catch (e) {
      if (e.code == "user-not-found") {
        toast("user not found");
      } else if (e.code == "wrong-password") {
        toast("Invalid email or password");
      }
    }
  }

  @override
  Future<void> signOut() async {
    await firebaseAuth.signOut();
  }

  @override
  Future<void> signUpUser(UserEntity user) async {
    try {
      await firebaseAuth.createUserWithEmailAndPassword(email: user.email!, password: user.password!).then((value) async{
        if (value.user?.uid != null) {
          await createUser(user);
        }
      });
      return;
    } on FirebaseAuthException catch (e) {
      if (e.code == "email-already-in-use") {
        toast("email is already taken");
      } else {
        toast("something went wrong");
      }
    }
  }

  @override
  Future<void> updateUser(UserEntity user) async {
    final userCollection = firebaseFirestore.collection(FirebaseConst.users);
    Map<String, dynamic> userInformation = Map();

    if (user.username != "" && user.username != null) userInformation['username'] = user.username;

    if (user.website != "" && user.website != null) userInformation['website'] = user.website;

    if (user.profileUrl != "" && user.profileUrl != null) userInformation['profileUrl'] = user.profileUrl;

    if (user.bio != "" && user.bio != null) userInformation['bio'] = user.bio;

    if (user.name != "" && user.name != null) userInformation['name'] = user.name;

    if (user.totalFollowing != null) userInformation['totalFollowing'] = user.totalFollowing;

    if (user.totalFollowers != null) userInformation['totalFollowers'] = user.totalFollowers;

    if (user.totalPosts != null) userInformation['totalPosts'] = user.totalPosts;


    userCollection.doc(user.uid).update(userInformation);

  }

}

				
			

If you’re confused about the FirebaseConst.users and also the toast message Go to lib > consts.dart and add the below code there.

 

These are basically the constants which we’re going to use again and again so we don’t wanna hardcode them so we created the constant for them.

				
					class FirebaseConst {
  static const String users = "users";
  static const String posts = "posts";
  static const String comment = "comment";
  static const String replay = "replay";

}

void toast(String message) {
  Fluttertoast.showToast(
      msg: message,
      toastLength: Toast.LENGTH_SHORT,
      gravity: ToastGravity.BOTTOM,
      timeInSecForIosWeb: 1,
      backgroundColor: blueColor,
      textColor: Colors.white,
      fontSize: 16.0);
}

				
			

Conclusion

Dependency injection is a programming technique that makes a class independent of its dependencies. It achieves that by decoupling the usage of an object from its creation. In the next article we’ll finally go for calling the methods in our UI and will see all the code we’ve done so far in action. In order to not miss the upcoming video be sure to subscribe to eTechViral and hit the bell Icon to make sure you get notified whenever new video is uploaded.

Website:
Have any Questions? Find me on
Share on facebook
Share
Share on twitter
Tweet
Share on linkedin
Share
Share on whatsapp
Share
Share on email
Send

Leave a Reply