DEV Community

Cover image for Flutter App, Simple animation with Hooks 🪝
Saad Alkentar
Saad Alkentar

Posted on

Flutter App, Simple animation with Hooks 🪝

What to expect from this tutorial?

This is a simple tutorial to add fading animation for a text field using flutter_hooks

usually, to add animation to any Flutter UI element, it has to be a state-full widget, the animation controller should be created in the state class, initState should be overwritten to populate the animation instance, and we should also override dispose to clean the memory from our animation controller when navigating to another screen!
It has always been a hurdle 😵, so I avoided using it as much as possible, but flutter_hooks has finally simplified the operation a bit, let's start by cleaning the home_screen code (you can start with your own screen of course).

The idea

Well, our diary is kinda similar to "Tom Riddle" diary (in "Harry Potter and the Chamber of Secrets"). And I liked how Harry's writing disappears, and Tom's answer appears in the diary. So, let's add fading animation to the text when sent to the AI and an appearing animation when received back from the AI. We will start by creating a simple fade-in and fade-out animation, then work on building app-specific behavior.

The behavior we wanna build is like this:

  1. The user clicks on the text field
  2. The app closes the keyboard (for the first time)
  3. The app sends a conversation request to the AI
  4. app fade in the server response
  5. The user clicks on the text field to start writing the response
  6. The app fades out the server response
  7. The user clicks the send button from the keyboard
  8. The app sends the message to the AI
  9. the app fades out the user text and fades in the server response

Clean home_screen code (optional)

If you are following this tutorial series, you will have some code in home_screen, so let's clean it a bit by using LayoutWidget from the previous article, removing screen contents, changing the action button to a logout button, and showing only the listen floating button

import 'package:alive_diary_app/config/dependencies.dart';
import 'package:alive_diary_app/config/router/app_router.dart';
import 'package:alive_diary_app/domain/repositories/preferences_repository.dart';
import 'package:alive_diary_app/presentation/widgets/layout_widget.dart';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'home_bloc.dart';


@RoutePage()
class HomeScreen extends HookWidget {
  const HomeScreen({super.key, this.title});

  final String? title;

  @override
  Widget build(BuildContext context) {
    final bloc = BlocProvider.of<HomeBloc>(context);

    final isLoading = useState(false);
    final speechText = useState("");

    return BlocListener<HomeBloc, HomeState>(
      listener: (context, state) {
        isLoading.value = state is HomeListeningState;

        if (state is HomeSTTState) {
          speechText.value = state.text ?? "";
        }

      },
      child: LayoutWidget(
        title: 'home'.tr(),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout, color: Colors.black,),
            onPressed: (){
              locator<PreferencesRepository>().logout();
              appRouter.replaceAll([LoginRoute()]);
            },
          )
        ],
        floatingActionButton: FloatingActionButton(
          tooltip: 'Listen',
          child: Icon(
            Icons.keyboard_voice_outlined,
            color: isLoading.value ? Colors.green : Colors.blue,
          ),
          onPressed: () => bloc.add(HomeSTTEvent()),
        ),
        child: Container(
          alignment: Alignment.center,
          child: const Text("Empty"),
        ),
      ),

    );
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/presentation/screens/home/home_screen.dart

The fun part 😁, the Animation!

To create animations with flutter_hooks, we start by creating the animation controller hook

    AnimationController animationController = useAnimationController(
        duration: const Duration(seconds: 4),
        initialValue: 1,
    );
Enter fullscreen mode Exit fullscreen mode

then we use it in the Transition component

              FadeTransition(
                opacity: animationController,
                child: Center(
                    child: Text("Hello World")
                ),
              ),
Enter fullscreen mode Exit fullscreen mode

that is it! no initState, no dispose, no headache

For our app, we need to show and hide text, so let's add two buttons to the home screen, one for showing and another one for hiding, it would look somewhat like

import 'package:alive_diary_app/config/dependencies.dart';
import 'package:alive_diary_app/config/router/app_router.dart';
import 'package:alive_diary_app/domain/repositories/preferences_repository.dart';
import 'package:alive_diary_app/presentation/widgets/layout_widget.dart';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_fonts/google_fonts.dart';
import 'home_bloc.dart';


@RoutePage()
class HomeScreen extends HookWidget {
  const HomeScreen({super.key, this.title});

  final String? title;

  @override
  Widget build(BuildContext context) {
    final bloc = BlocProvider.of<HomeBloc>(context);

    final isLoading = useState(false);
    final speechText = useState("");

    AnimationController animationController = useAnimationController(
        duration: const Duration(seconds: 4),
        initialValue: 1,
    );

    return BlocListener<HomeBloc, HomeState>(
      listener: (context, state) {
        isLoading.value = state is HomeListeningState;

        if (state is HomeSTTState) {
          speechText.value = state.text ?? "";
        }

      },
      child: LayoutWidget(
        title: 'home'.tr(),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout, color: Colors.black,),
            onPressed: (){
              locator<PreferencesRepository>().logout();
              appRouter.replaceAll([LoginRoute()]);
            },
          )
        ],
        floatingActionButton: FloatingActionButton(
          tooltip: 'Listen',
          child: Icon(
            Icons.keyboard_voice_outlined,
            color: isLoading.value ? Colors.green : Colors.blue,
          ),
          onPressed: () => bloc.add(HomeSTTEvent()),
        ),
        child:Container(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              FadeTransition(
                opacity: animationController,
                child: Center(
                    child: Text("Hello World",
                      style: GoogleFonts.caveat(
                        fontSize: 30,
                        color: Colors.black,
                      ),
                    )
                ),
              ),
              ElevatedButton(
                child: Text("show".tr()),
                onPressed: () async {
                  animationController.forward();
                },
              ),

              ElevatedButton(
                child: Text("hide".tr()),
                onPressed: (){
                  animationController.animateBack(0,
                      duration: const Duration(seconds: 1),
                  );
                },
              )

            ],
          ),
        ),
      ),

    );
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/presentation/screens/home/home_screen.dart

We are using google_fonts to make the font more human-like 🤓, with google fonts, you are free to choose from a wide range of great fonts, but feel free to ignore it since it doesn't have to do with the animation itself 😅

The home UI should look somewhat like

hidden shown
home screen hidden home screen shown

The text should be visible in the initial state, clicking hide should fade it away, and clicking show should fade it in. We can change the initial text state to hidden by changing initialValue to 0.

simple and great! no need to run from UI animation anymore 😁, now for the app behavior, let's start working on it

Diary app text field behavior

Let's start by creating a large text field, with a nice paper-like background, and address the onTab and onSubmit actions

...
  Widget build(BuildContext context) {
    final textController = useTextEditingController();

...
      child: LayoutWidget(
        title: 'home'.tr(),
...
        child:Container(
          height: double.infinity,
          padding: const EdgeInsets.symmetric(horizontal: 15),
          decoration: const BoxDecoration(
            image: DecorationImage(
              image: AssetImage("assets/images/paper_bg.jpg"),
              fit: BoxFit.cover,
            ),
          ),
          child: TextField(
            decoration: const InputDecoration(border: InputBorder.none),
            cursorHeight: 35,
            style: GoogleFonts.caveat(
              fontSize: 30,
              color: Colors.black
            ),
            keyboardType: TextInputType.multiline,
            textInputAction: TextInputAction.send,
            controller: textController,
            maxLines: null,
            onTap: () async {

            },
            onSubmitted: (text) async {

            },
          ),
        ),
      ),
...
Enter fullscreen mode Exit fullscreen mode

lib/presentation/screens/home/home_screen.dart

We have added a text field, with a background, multiple lines support, and functions for clicking and submitting (the software keyboard submit button), let's start with how to dismiss the software keyboard?

Dismiss the software keyboard in flutter

How to dismiss the software keyboard? it is kinda tricky, in Android, we start by adding PROCESS_TEXT query in the manifest at the same level as the application section

...
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>

    <application
        android:label="alive_diary_app"
        android:name="${applicationName}"
        android:icon="@mipmap/launcher_icon">
...
Enter fullscreen mode Exit fullscreen mode

android/app/src/main/AndroidManifest.xml

Now we can test it in the app with the TextInput.hide method as

import 'package:flutter/services.dart';

...
            child: TextField(
              decoration: const InputDecoration(border: InputBorder.none),
              cursorHeight: 35,
              style: GoogleFonts.caveat(
                fontSize: 30,
                color: Colors.black,
              ),
              keyboardType: TextInputType.multiline,
              textInputAction: TextInputAction.send,
              controller: textController,
              maxLines: null,
              onTap: () async {
                await Future.delayed(const Duration(seconds: 2));
                SystemChannels.textInput.invokeMethod('TextInput.hide');

              },
...
Enter fullscreen mode Exit fullscreen mode

lib/presentation/screens/home/home_screen.dart

The keyboard will appear when clicking on the text field, then after two seconds, it will just disappear.

Text field animation

We want to apply the animation on the text field, so it must be wrapped with FadeTransition, and we need two functions

  • showText function, which hides the keyboard, sets the new value and fades it in
  • clearText function, which fades out field contents, and sets it to an empty text.
...
  Widget build(BuildContext context) {
    final isLoading = useState(false);
    final speechText = useState("");
    final canWrite = useState<bool?>(null);
    final textController = useTextEditingController();

...
    void showText(String? text) async {
      await Future.delayed(const Duration(milliseconds: 100));
      SystemChannels.textInput.invokeMethod('TextInput.hide');
      animationController.animateBack(0, duration: const Duration(seconds: 1));
      await Future.delayed(const Duration(seconds: 2));
      textController.text = text ?? "";
      animationController.forward();
    }

    void clearText() async {
      animationController.animateBack(0, duration: const Duration(seconds: 1));
      await Future.delayed(const Duration(seconds: 2));
      textController.text = "";
      animationController.forward();
    }
...
            child: TextField(
              decoration: const InputDecoration(border: InputBorder.none),
              cursorHeight: 35,
              style: GoogleFonts.caveat(
                fontSize: 30,
                color: Colors.black,
              ),
              keyboardType: TextInputType.multiline,
              textInputAction: TextInputAction.send,
              controller: textController,
              maxLines: null,
              onTap: () async {

                if (canWrite.value == null) {
                  showText("How was your day?");
                  canWrite.value = true;
                } else if (canWrite.value == true) {
                  clearText();
                }

              },
              onSubmitted: (text) async {
                showText("Tell me more");
              },
            ),
...
Enter fullscreen mode Exit fullscreen mode

lib/presentation/screens/home/home_screen.dart

That is it! well, almost, we are mimicking the server response with a plain text, but whatever, let's concentrate on the behavior for now

we have created the two functions showText and clearText, we have created a new helper state canWrite in order to distinguish the first click,
if this is the first click, we show the server response (static text for now), but if this is not the first time, then we simply clear the text.
when clicking the keyboard submit button, we (should send the text to server and show its response) show the message "Tell me more"

This is the required behavior as described, let's continue building the app together and as always

Stay tuned 😎

Top comments (0)