Dogly - a mobile app with Flutter: articles

3 minute read

Introduction

In this post, I will create a view with articles about dog care. Every ten articles will be generated using Chat GPT, so the content might not be true. This feature will have the presentation layer only, because every markdown article file will be stored locally as an asset. If I were creating a real application, I would use some kind of content management system for easy and pleasant content handling. It will be a super short post again, because of the simplicity of the feature.

Let’s get to work!

Article card

The article card is just a rectangle with the cover photo and the title. It should navigate to the article view on click, so a gesture detector is required here. The widget takes the paths for article and image assets and the title as an input argument. The card’s content is displayed in the column, so to be sure it keeps the fixed size I use the IntrinsicHeight class. The ClipRRect is used for rounding the corners of the asset image.

class ArticleCard extends StatelessWidget {
  final String article;
  final String title;
  final String image;

  const ArticleCard({
    Key? key,
    required this.article,
    required this.title,
    required this.image,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (BuildContext context) => ArticleView(article: article),
          ),
        );
      },
      child: Card(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
        child: IntrinsicHeight(
          child: Column(
            children: [
              ClipRRect(
                borderRadius: BorderRadius.circular(8),
                child: Image(
                  image: AssetImage('assets/articles_images/$image'),
                  height: 200,
                  width: double.infinity, // Take available width
                  fit: BoxFit.cover,
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8.0),
                child: Text(
                  title,
                  style: const TextStyle(
                      fontWeight: FontWeight.bold, fontSize: 16),
                  textAlign: TextAlign.center,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

It looks like this in the application:

article_card

Article view

For rendering markdown text I use the flutter_markdown package. I load the file from the assets using a FutureBuilder. If the snapshot has data it renders the content, else the progress indicator.

class ArticleView extends StatelessWidget {
  final String article;
  
  const ArticleView({super.key, required this.article});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'Article 📰',
        ),
        centerTitle: true,
      ),
      body: FutureBuilder(
          future: rootBundle.loadString("assets/articles/$article"),
          builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
            if (snapshot.hasData) {
              return Markdown(data: snapshot.data!);
            }

            return const Center(
              child: CircularProgressIndicator(),
            );
          }),
    );
  }
}

It looks like this:

article_1

article_2

Articles view

The articles view is just a list generated from the collection of maps with article data. I keep it in the application code for now, but it can be done in a gentler way using CMS for example.

class ArticlesView extends StatelessWidget {
  const ArticlesView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    List<Map> articles = [
      {
        "article": "benefits_of_dog.md",
        "title": "The Benefits of Owning a Dog",
        "image": "benefits_of_dog.png"
      },
      {
        "article": "dog_tips_for_new.md",
        "title": "Basic Dog Care Tips for New Owners",
        "image": "dog_tips_for_new.png"
      },
      {
        "article": "popular_breeds.md",
        "title": "Popular Dog Breeds: Choosing the Right One for You",
        "image": "popular_breeds.png"
      },
      {
        "article": "dog_body_language.md",
        "title":
            "Understanding Dog Body Language: What Your Dog is Trying to Tell You",
        "image": "dog_body_language.png"
      },
      {
        "article": "dog_games.md",
        "title": "Fun and Engaging Games to Play with Your Dog",
        "image": "dog_games.png"
      },
      {
        "article": "socializing.md",
        "title": "The Importance of Socializing Your Dog",
        "image": "socializing.png"
      },
      {
        "article": "how_to_train.md",
        "title": "How to Train Your Dog: Basic Commands and Techniques",
        "image": "how_to_train.png"
      },
      {
        "article": "dog_health.md",
        "title":
            "Preventing Common Dog Health Issues: Tips for a Happy and Healthy Pet",
        "image": "dog_health.png"
      },
      {
        "article": "emotional_support.md",
        "title": "The Role of Dogs in Therapy and Emotional Support",
        "image": "emotional_support.png"
      },
    ];
    return ListView.builder(
        itemCount: articles.length,
        itemBuilder: ((context, index) {
          return ArticleCard(
            article: articles[index]["article"],
            title: articles[index]["title"],
            image: articles[index]["image"],
          );
        }));
  }
}

It works like this:

articles_view

Conclusion

This feature was really easy to implement but quite satisfying. I think this view looks nice, however, I do not have good taste at all. In the next, I will implement the calendar feature and it will be the last from the series. It will be also much longer and more complicated. Cya there!