Complete Flutter app walkthrough part 1

Erik
Dreamwod tech
Published in
6 min readFeb 9, 2021

Components, best practices, and tools used to build a large Flutter app.

The Dreamwod app

This is the first post in a series on how the Dreamwod app is built. The Dreamwod app is an iOS and Android app for CrossFit athletes where they can track their progress and for CrossFit gyms that want their athletes to engage in their community. We have previously described how we have created some of the backend services (Golang + CloudRun at Google Cloud) and now it’s time to discuss the app.

The series contains (at least) the following posts.

  1. Components used for building the details page (this post).
  2. Flutter authentication flow with bloc pattern.
  3. Flutter + dio framework + best practices
  4. Components used for building the profile page (available later).
  5. Integrating Stripe for payments (available later).
  6. Build and publish with Fastlane (available later).

Scoreboard screen

In the Dreamwod app, athletes register their scores in different daily workouts. The scoreboard result screen displays the scores and athletes can navigate to a certain score to check out the details. This post focus on the details page and the different components used to build it.

Score screen in the Dreamwod app

The scoreboard results screen is pretty simple with different list tiles group by different categories. The focus of this post is the details page for the scores.

Details page

The details page is the page where a score and comment about a workout is displayed. The functionality on this page is.

  • Pull down to refresh the page.
  • Like/unlike the workout/score.
  • Add a comment (text and or emoji).
  • Add a gif comment (Giphy integration).
  • List comments and scroll to the latest one added.
  • Display nicely formatted posted date on comments.
  • Facebook style reactions (with 😂, 👍 or other emojis) on the comments.
  • Add reactions and display a chip/badge to show the number of reactions

Additional functionality not seen on the image below is also

  • Loading overlay
  • Haptic feedback
Animation of the details page

Loading overlay

The component loading_overlay is used to display a loading overlay when data from the backend is loaded. isLoading is initialized to true and then unset when the data from the backend is available.

Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar(
title: Text('Workout result'),
),
body: LoadingOverlay(
isLoading: isLoading,
child: ... )
);

Pull to refresh

The package pull_to_refresh is used for refreshing when the user pulls down.

Pull down to refresh the screen
SmartRefresher(
enablePullDown: true,
header: ClassicHeader(),
onRefresh: onRefresh,
controller: refreshController,
child: ListView(controller: scrollController, children:
getBody())));

Like button with animation

The package like_button is used for creating the animations that are displayed when a user likes a workout. The logic is pretty straight forward and the like and count builders can be used to display different messages depending on how many likes there are.

Animations on the like button
LikeButton(
onTap: (onLikeButtonTapped),
size: 26,
isLiked: isLiked,
likeCount: numberOfLikes,
likeCountAnimationType: LikeCountAnimationType.all,
likeBuilder: (bool isLiked) {
return Icon(
Icons.favorite,
color: isLiked ? Colors.pinkAccent : Colors.grey,
size: 26,
);
},
countBuilder: (int count, bool isLiked, String text) {
if (count == 0) {
return Text('Tap to like!');
} else if (count == 1 && isLiked) {
return Text('You like this!');
}

return Text('${count} ${(count == 1 ? 'athlete' : 'athletes')} likes this'
);
},
),

Emoji sizing

The default size of emojis in flutter text is a bit too small for us. We want to scale them up so they stand out more around the text.

The normal size of emojis in text
Emojis scaled up 30%

This can be achieved by a component that scales up the text.

class EmojiText extends StatelessWidget {
final String text;
final TextStyle style;
final double emojiFontMultiplier;

const EmojiText({
@required this.text,
@required this.style,
this.emojiFontMultiplier = 1.3,
});

// Regex to match emojis
static final regex = RegExp(
'((?:\u00a9|\u00ae|[\u2000-\u3300]|\ufe0f|[\ud83c-\ud83e][\udc00-\udfff]|\udb40[\udc61-\udc7f])+)');

List<TextSpan> generateTextSpans(String text) {
var spans = <TextSpan>[];
final emojiStyle = style.copyWith(
fontSize: (style.fontSize * emojiFontMultiplier),
letterSpacing: 2,
);

text.splitMapJoin(
regex,
onMatch: (m) {
spans.add(
TextSpan(
text: m.group(0),
style: emojiStyle,
),
);
return '';
},
onNonMatch: (s) {
spans.add(TextSpan(text: s));
return '';
},
);
return spans;
}

@override
Widget build(BuildContext context) {
return Container(
child: RichText(
text: TextSpan(children: generateTextSpans(text), style: style),
),
);
}
}

We also have a utility function that can be used to check if all characters in a string are emojis so that the text can be scaled even more.

class Utils {
static final emailRegExp = RegExp(
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$');

static final onlyEmojiRegExp = RegExp(
'^(\u00a9|\u00ae|[\u2000-\u3300]|\ufe0f|[\ud83c-\ud83e][\udc00-\udfff]|\udb40[\udc61-\udc7f]|\s|\t|\n|\r)*\$');

static bool isOnlyEmojis(String comment) {
return onlyEmojiRegExp.hasMatch(comment);
}
}

Used for example like below

EmojiText(
text: comment,
style: style,
emojiFontMultiplier: Utils.isOnlyEmojis(comment) ? 3: 1.4,
);

Display time

The component used to format time nicely is called timeago. It is that component that will format dates like “a moment ago”, “24 hours ago” etc.

Facebook-style reaction button & badges

Facebook style reaction button and badges

The package used to create the reaction buttons is the package flutter_reaction_button. A flutter chip could have been used to create the badge counting the reactions but it is often too large even if with padding set to zero. The badges package is the rescue!

Gif support through giphy

We are using the giphy_picker package when adding images. The only code needed is below. The package contains all the logic and the only thing needed is to pick the URL and store it in the backend.

GestureDetector(
onTap: () async {
final gif = await GiphyPicker.pickGif(
context: context,
apiKey: 'your-api-key');

if (gif != null) {
addGif(gif);
}
},
child: Icon(Icons.gif_rounded));
void addGif(GiphyGif gif) {
BlocProvider.of<ScoreBloc>(context).add(AddComment(
url: gif.images.original.url));

setState(() {
// Todo: reset the focus in a better way
FocusScope.of(context).requestFocus(FocusNode());
});
}
Adding a gif image as a comment

Cupertino rounded corners

A great flutter package to make Cupertino rounded corners (also referred to as squircles) is the cupertino_rounded_corners. See the example in the emoji scaling section.

Cached network image

Users will look at the same comments multiple times so it makes sense to cache the gif animations. An excellent package for this is cached_network_image. It also supports a loading placeholder and an error widget.

CachedNetworkImage(
imageUrl: url,
placeholder: (context, url) => Padding(
padding: const EdgeInsets.symmetric(
vertical: 40, horizontal: 80),
child: Container(
child: CircularProgressIndicator(),
),
),
errorWidget: (context, url, error) =>
Icon(Icons.error),
);

Haptic feedback

Haptic feedback can be added when for example the like button is pressed.

Future<bool> onLikeButtonTapped(bool isLiked) async {
// todo, code for handling the event

await HapticFeedback.lightImpact();

return !isLiked;
}

That's all for the first part!

--

--

Erik
Dreamwod tech

Developer, backend, frontend, ML. Likes crossfit and training. Building on the app dreamwod.app.