thumb

Instagram Clone Clean Architecture – Part 10

Table of Contents
utilizing - Instagram Clone Clean Architecture

The time we have waited so far is finally here, in this part we will finally be calling the methods in our UI and you’ll see all the code up until know we wrote in action.

This is just single part of this series. Checkout all the other parts here and learn to build clean and maintainable apps.

Note: In our sign up page, we also have to upload profile image of user. If you are familiar with Firebase we upload our files and images in Cloud Storage. That is separate topic and we will cover it individually in the next part.

First move to your main.dart and change it from:

				
					MaterialApp(
  debugShowCheckedModeBanner: false,
  title: "Instagram Clone",
  darkTheme: ThemeData.dark(),
  onGenerateRoute: OnGenerateRoute.route,
  initialRoute: "/",
  routes: {
    "/": (context) {
      return MainScreen();
    }
  },
),

				
			

To:

				
					MaterialApp(
  debugShowCheckedModeBanner: false,
  title: "Instagram Clone",
  darkTheme: ThemeData.dark(),
  onGenerateRoute: OnGenerateRoute.route,
  initialRoute: "/",
  routes: {
    "/": (context) {
      return BlocBuilder<AuthCubit, AuthState>(
        builder: (context, authState) {
          if (authState is Authenticated) {
            return MainScreen(uid: authState.uid,);

          } else {
            return SignInPage();
          }
        },
      );
    }
  },
),

				
			

We have Bloc Builder and Bloc Listener.

Bloc Builder:

The Bloc Builder is used when we want to draw a widget based on what is the current state.

Bloc Listener:

The Bloc Listener is keep listening to the new changes in state and do not return a widget.

In the code above using Bloc Builder we’re checking if our AuthState is Authenticated, so return the MainScreen() widget otherwise return SignInPage() widget.

 

Notice: The MainScreen() accepts uid argument. Go to MainScreen() and create a final field and pass it in the constructor of Main Screen StatefulWidget. We will use this current user id later in this page.

				
					class MainScreen extends StatefulWidget {
  final String uid;

  const MainScreen({Key? key, required this.uid}) : super(key: key);

				
			

With this wrap your Material App with MultiBlocProvider which accepts list of BlocProviderSingleChildWidget. In this list requests to the Cubits from their registered types which are located in the injection container.

				
					MultiBlocProvider(
  providers: [
    BlocProvider(create: (_) => di.sl<AuthCubit>()..appStarted(context)),
    BlocProvider(create: (_) => di.sl<CredentialCubit>()),
    BlocProvider(create: (_) => di.sl<UserCubit>()),
    BlocProvider(create: (_) => di.sl<GetSingleUserCubit>()),
  ],
  child: MaterialApp(
    ...

				
			

User is our first priority so we request to all the user cubits inside the injection container they will be called when our app starts and as they are factory methods so will give us the new instance each time.

Notice: I called the appStarted(context) methods from our AuthCubit this is because, this appStarted is the method which ensure if the user is sign in or not.

Now if you hot restart your application you will see sign in page on your emulator. This is because our user is unauthenticated and the Bloc Builder draw the widget based on the current state (UnAuthenticated State).

Sign Up Page:

Go to sign_up_page and convert stateless widget to stateful widget. Now you need to add some controllers (TextEditingController) and pass them in the forms (FormContainerWidget).

 

Down below the state do this:

				
					TextEditingController _emailController = TextEditingController();
TextEditingController _usernameController = TextEditingController();
TextEditingController _passwordController = TextEditingController();
TextEditingController _bioController = TextEditingController();

bool _isSigningUp = false;

				
			

Notice: This _isSigningUp Boolean variable will be used when to show circular progress indicator when the user is signing up.

Also not forget to dispose these methods.

				
					@override
void dispose() {
  _emailController.dispose();
  _passwordController.dispose();
  _bioController.dispose();
  _usernameController.dispose();
  super.dispose();
}

				
			

Now search for ButtonContainerWidget() with the text “Sign Up”. You will found onTapListener there, simply call the method here _signUpUser().

				
					ButtonContainerWidget(
  color: blueColor,
  text: "Sign Up",
  onTapListener: () {
    _signUpUser();
  },
),

				
			

Implement this method:

				
					void _signUpUser() {
  setState(() {
    _isSigningUp = true;
  });
  BlocProvider.of<CredentialCubit>(context).signUpUser(
      user: UserEntity(
        email: _emailController.text,
        password: _passwordController.text,
        bio: _bioController.text,
        username: _usernameController.text,
        totalPosts: 0,
        totalFollowing: 0,
        followers: [],
        totalFollowers: 0,
        profileUrl: "",
        website: "",
        following: [],
        name: "",
      )
  ).then((value) => _clear());
}

_clear() {
  setState(() {
    _usernameController.clear();
    _bioController.clear();
    _emailController.clear();
    _passwordController.clear();
    _isSigningUp = false;
  });
}

				
			

Notice: We don’t pass the uid, this is because it is handled in the backend. Also Notice the _isSigningUp variable when the method is called it becomes true by setState and later in the clear method it become false in setState when the process end, through this variable we will control when and where to show circular progress indicator.

Right after the button container widget check if the variable is true show some text with indicator otherwise an empty container widget.

				
					ButtonContainerWidget(
  color: blueColor,
  text: "Sign Up",
  onTapListener: () {
    _signUpUser();
  },
),
sizeVer(10),
_isSigningUp == true ? Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Text("Please wait", style: TextStyle(color: primaryColor, fontSize: 16, fontWeight: FontWeight.w400),),
    sizeHor(10),
    CircularProgressIndicator()
  ],
) : Container(width: 0, height: 0,),

				
			

Now separate the body of Scaffold in a separate method.

				
					_bodyWidget(){...}
				
			

And pass this body widget in the scaffold body like:

				
					Scaffold(
    backgroundColor: backGroundColor,
    body: _bodyWidget()
);

				
			

Now wrap the body with the Bloc Consumer.

Bloc Consumer:

The Bloc Consumer is the mix up between Bloc Builder and Bloc Listener. This is used when we want to draw something based on the current state and want to execute some actions depending on the new arriving state.

In the code below we execute two actions in listener first if the state is Credential Success then call the method from AuthCubit loggedIn() which means pass the uid in the Authenticated state, second if there is some failure show toast. In the builder we again call the bloc builder to check again if the Authenticated state has uid if so, move user to MainScreen otherwise keep it on the same page.

				
					Scaffold(
    backgroundColor: backGroundColor,
    body: BlocConsumer<CredentialCubit, CredentialState>(
      listener: (context, credentialState) {
        if (credentialState is CredentialSuccess) {
          BlocProvider.of<AuthCubit>(context).loggedIn();
        }
        if (credentialState is CredentialFailure) {
          toast("Invalid Email and Password");
        }
      },
      builder: (context, credentialState) {
        if (credentialState is CredentialSuccess) {
          return BlocBuilder<AuthCubit, AuthState>(
            builder: (context, authState) {
              if (authState is Authenticated) {
                return MainScreen(uid: authState.uid);
              } else {
                return _bodyWidget();
              }
            },
          );
        }
        return _bodyWidget();
      },
    )

				
			

Congrats 🎉 test your sign up page and you are good to go.

Sign In Page:

Sign in page will not be that much different but only one method will be different.

First copy the controllers from Sign up Page not all but only two (email and password) also dispose them.

Now Search for the ButtonContainerWidget and in the onTapListener call the _signInUser() method.

Implement it then:

				
					void _signInUser() {
  setState(() {
    _isSigningIn = true;
  });
  BlocProvider.of<CredentialCubit>(context).signInUser(
    email: _emailController.text,
    password: _passwordController.text,
  ).then((value) => _clear());
}

_clear() {
  setState(() {
    _emailController.clear();
    _passwordController.clear();
    _isSigningIn = false;
  });
}

				
			

Notice: Here the _isSigningIn variable is same, copy the code from Sign up Page, right after the ButtonContainerWidget; and paste it in Sign in Page in the same place. Only change the name of the variable.

Lastly separate the body widget and copy the code from Sign up Page and paste it everything will remain the same.

Congrats 🎉 test your sign in page and you are good to go.

Logout User:

Go to Profile Page and come to bottom you will see bottom modal sheet there. Wrap the text widget “Logout” with InkWell and call this onTap method:

				
					InkWell(
  onTap: () {
    BlocProvider.of<AuthCubit>(context).loggedOut();
    Navigator.pushNamedAndRemoveUntil(context, PageConst.signInPage, (route) => false);
  },
  child: Text(
    "Logout",
    style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16, color: primaryColor),
  ),

				
			

Congrats 🎉 test the logout button and you are good to go.

Load User data from Firestore Database:

Load Single User on MainScreen:

If you remember, we created the getSingleUser to get the current single user. Go to MainScreen() and wrap the Scaffold with Bloc Builder and pass the GetSingleUserCubit and GetSingleUserState, and check if the state is GetSingleUserLoaded then retrieve the current single user from the state and save it in a variable.

				
					BlocBuilder<GetSingleUserCubit, GetSingleUserState>(
  builder: (context, getSingleUserState) {
    if (getSingleUserState is GetSingleUserLoaded) {
      final currentUser = getSingleUserState.user;
      return Scaffold(
        backgroundColor: backGroundColor,
        bottomNavigationBar: CupertinoTabBar(
          backgroundColor: backGroundColor,
          items: [
            BottomNavigationBarItem(icon: Icon(MaterialCommunityIcons.home_variant, color: primaryColor), label: ""),
            BottomNavigationBarItem(icon: Icon(Ionicons.md_search, color: primaryColor), label: ""),
            BottomNavigationBarItem(icon: Icon(Ionicons.md_add_circle, color: primaryColor), label: ""),
            BottomNavigationBarItem(icon: Icon(Icons.favorite, color: primaryColor), label: ""),
            BottomNavigationBarItem(icon: Icon(Icons.account_circle_outlined, color: primaryColor), label: ""),

          ],
          onTap: navigationTapped,
        ),
        body: PageView(
          controller: pageController,
          children: [
            HomePage(),
            SearchPage(),
            UploadPostPage(),
            ActivityPage(),
            ProfilePage(currentUser: currentUser,)
          ],
          onPageChanged: onPageChanged,
        ),
      );
    }
    return Center(child: CircularProgressIndicator(),);
  },
);

				
			

After doing this, if you hot restart your application you will see circular progress indicator on your emulator.

 

To avoid this in the same page (MainScreen), in the initState call the getSingleUser method from the GetSingleUserCubit and pass the uid inside.

				
					@override
void initState() {
  BlocProvider.of<GetSingleUserCubit>(context).getSingleUser(uid: widget.uid);
  pageController = PageController();
  super.initState();
}

				
			

Now the indicator will be gone.

Only last thing to show up current user data from firestore database in the profile page.

Go to Profile Page create a final field UserEntity and pass it in the constructor.

				
					class ProfilePage extends StatelessWidget {
  final UserEntity currentUser;
  const ProfilePage({Key? key, required this.currentUser}) : super(key: key);

				
			

You will see compile time error in MainScreen() page. Now in the list of pages one of them Profile Page require currentUser (UserEntity) as we got the current user from GetSingleUserCubit and is stored in a variable currentUser so pass it simply in here.

				
					PageView(
  controller: pageController,
  children: [
    HomePage(),
    SearchPage(),
    UploadPostPage(),
    ActivityPage(),
    ProfilePage(currentUser: currentUser,)
  ],
  onPageChanged: onPageChanged,
),

				
			

That was it for the Main Screen.

Profile Page:

Access all the data from the UserEntity in the profile page by removing all the dummy data.

profile_page.dart

				
					class ProfilePage extends StatelessWidget {
  final UserEntity currentUser;
  const ProfilePage({Key? key, required this.currentUser}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: backGroundColor,
      appBar: AppBar(
        backgroundColor: backGroundColor,
        title: Text("${currentUser.username}", style: TextStyle(color: primaryColor),),
        actions: [
          Padding(
            padding: const EdgeInsets.only(right: 10.0),
            child: InkWell(
            onTap: () {
              _openBottomModalSheet(context);
            },child: Icon(Icons.menu, color: primaryColor,)),
          )
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10),
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Container(
                    width: 80,
                    height: 80,
                    decoration: BoxDecoration(
                      color: secondaryColor,
                      shape: BoxShape.circle
                    ),
                  ),
                  Row(
                    children: [
                      Column(
                        children: [
                          Text("${currentUser.totalPosts}", style: TextStyle(color: primaryColor, fontWeight: FontWeight.bold),),
                          sizeVer(8),
                         Text("Posts", style: TextStyle(color: primaryColor),)
                        ],
                      ),
                      sizeHor(25),
                      Column(
                        children: [
                          Text("${currentUser.totalFollowers}", style: TextStyle(color: primaryColor, fontWeight: FontWeight.bold),),
                          sizeVer(8),
                          Text("Followers", style: TextStyle(color: primaryColor),)
                        ],
                      ),
                      sizeHor(25),
                      Column(
                        children: [
                          Text("${currentUser.totalFollowing}", style: TextStyle(color: primaryColor, fontWeight: FontWeight.bold),),
                          sizeVer(8),
                          Text("Following", style: TextStyle(color: primaryColor),)
                        ],
                      )
                    ],
                  )
                ],
              ),
              sizeVer(10),
              Text("${currentUser.name == ""? currentUser.username : currentUser.name}", style: TextStyle(color: primaryColor,fontWeight: FontWeight.bold),),
              sizeVer(10),
              Text("${currentUser.bio}", style: TextStyle(color: primaryColor),),
              sizeVer(10),
              GridView.builder(itemCount: 32, physics: ScrollPhysics(), shrinkWrap: true,gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 5, mainAxisSpacing: 5), itemBuilder: (context, index) {
                return Container(
                  width: 100,
                  height: 100,
                  color: secondaryColor,
                );
              })
            ],
          ),
        ),
      )
    );
  }
}

				
			

Solution Code:

GitHub: https://github.com/AdnanKhan45/instagram-app-clone

Conclusion:

We’ve successfully called all the methods in our UI. In the next part we will learn about Firebase Cloud Storage and also to edit user profile. That will be the last part of user and then we will go for the next steps. 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: https://www.etechviral.com

YouTube:  https://www.youtube.com/channel/UCO6gMNHYhRqyzbskNh4gG_A

eTechViral Instagram: https://www.instagram.com/etechviral/

Have any Questions? Find me on:

GitHub: https://github.com/AdnanKhan45

LinkedIn: https://www.linkedin.com/in/adnan-khan-23bb8821b/

Instagram: https://www.instagram.com/dev.adnankhan

Twitter: https://twitter.com/Adnan54M

Facebook: https://web.facebook.com/profile.php?id=100011793480431

Share on facebook
Share
Share on twitter
Tweet
Share on linkedin
Share
Share on whatsapp
Share
Share on email
Send

Leave a Reply