Flutter CI/CD | Building, Code signing, and Publishing

Posted February 12, 2024
Easy Example Tutorial to Using Node-Red with SQlite

In this article, we will build a Flutter CI/CD application and then deploy it to Google Play Store and App store via code magic.

You will be:

  • Creating a sample Flutter Authors App
  • Writing relevant test.
  • Connecting the Flutter with CI/CD
  • Setting up the build cicd workflow on codemagic
  • Deploying on codemagic to Google Play store

To Proceed in this article, it is important to have the following:

  • Flutter installed on your computer.
  • Prior experience working with Flutter.

Setting up the application

  • Proceed to your preferred working directory.

  • In it, initialize a flutter application:

    flutter create authors_app
    
  • Proceed to the newly created directory:

    cd authors_app
    
  • Start the development server:

    flutter run
    

Creating a sample Flutter Authors App

  • In the lib directory, create a Providers directory.

  • In the Providers directory, create a get_task_provider.dart.

  • In the get_task_provider.dart:

    • Import the required packages:

      import 'dart:convert';
      import 'package:flutter/foundation.dart';
      import 'package:http/http.dart' as http;
      
    • Define a GetTaskProvider class:

      class GetTaskProvider extends ChangeNotifier {
      }
      
    • Define the variables:

        String _response = '';
        dynamic _list = [];
        String get getResponse => _response;
      
    • Define a function for getting the authors:

      void getAuthors(bool isLocal) async {
          final response = await http
          .get(Uri.parse('https://quotable.io/authors'));
          if (response.statusCode == 200) {
              _list = jsonDecode(response.body);
          } else {
              throw Exception('Failed to load authors');
          }
      }
      
    • Define a function for extracting the response:

      dynamic getResponseData() {
          if (_list.isNotEmpty) {
          final data = _list;
          return data['results'] ?? {};
          } else {
          return {};
          }
      }
      
  • In the lib directory, create a Screens directory. Inside it, create a home_page.dart file.

  • In the home_page.dart:

    • Import the necessary packages:

      import 'package:flutter/material.dart';
      import 'package:graphql_app/Providers/get_task_provider.dart';
      import 'package:provider/provider.dart';
      
    • Define the home page stateful widget:

      class HomePage extends StatefulWidget {
      const HomePage({Key? key}) : super(key: key);
      
      @override
      _HomePageState createState() => _HomePageState();
      }
      
      class _HomePageState extends State<HomePage> {
      
      }
      
    • In the _HomePageState fetch the authors and show a list:

      class _HomePageState extends State<HomePage> {
      bool isFetched = false;
      @override
      Widget build(BuildContext context) {
          return Scaffold(
          appBar: AppBar(
              title: const Text('Authors Home'),
          ),
          body: Consumer<GetTaskProvider>(builder: (context, task, child) {
              if (isFetched == false) {
              task.getAuthors(true); // get the authors
              Future.delayed(const Duration(seconds: 3),  () => isFetched = true);
              }
              return RefreshIndicator(
              onRefresh: () {
                  task.getAuthors(false);
                  return Future.delayed(const Duration(seconds: 3));
              },
              child: CustomScrollView(
                  slivers: [
                  SliverToBoxAdapter(
                      child: Container(
                      height: MediaQuery.of(context).size.height,
                      padding: const EdgeInsets.all(20),
                      child: Column(
                          children: [
                          Container(
                              margin: const EdgeInsets.all(20),
                              child: const Text("Available authors"),
                          ),
                          if (task.getResponseData().isEmpty)
                          const Text('No Author found'),
                          Expanded(
                              child: ListView(
                              children: List.generate(task.getResponseData().length,
                                  (index) {
                              final data = task.getResponseData()[index];
                              return ListTile(
                                  contentPadding: const EdgeInsets.all(0),
                                  title: Text(data['name']),
                                  subtitle: Text(data['description']),
                                  leading: CircleAvatar(
                                  backgroundColor: Colors.grey,
                                  child: Text((index + 1).toString()),
                                  ),
                                  trailing:  Row(
                                      mainAxisSize:MainAxisSize.min,
                                      children:[
                                      IconButton(
                                      onPressed: () { // Proceed to the specific author
      
                                      },
                                      icon: const Icon(Icons.arrow_right_rounded)),
                                      ]
                                  )
                              );
                              }),
                          )),
                          const SizedBox(height: 150),
                          ],
                      ),
                      ),
                    )
                  ],
              ),
              );
          })
          );
      }
      }
      
  • In the main.dart:

    • Import the necessary packages:

      import 'package:flutter/material.dart';
      import 'package:graphql_app/Providers/get_task_provider.dart';
      import 'package:graphql_flutter/graphql_flutter.dart';
      import 'package:provider/provider.dart';
      import 'Screens/home_page.dart';
      
    • In the build function, return the Provider as below:

      return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => GetTaskProvider()),
      ],
      child: MaterialApp(
        home: HomePage(),
        theme: ThemeData(
          primaryColor: Colors.blue,
          primarySwatch: Colors.blue
        )
      ));
      
  • Ensure that the development server is up and running. Your home page should be simialr to:

home_page

Showing a specific author

  • In the lib/Screens directory, create a author_page.dart.

  • In the author_page.dart:

    • Import the necessary dependecies:

      import 'package:flutter/material.dart';
      
    • Define a AuthorPage stateful widget that will accept the positional arguments:

      class AuthorPage extends StatefulWidget {
          final String slug;
          final String name;
          final String description;
          const AuthorPage({required this.slug,required this.name,required this.description,Key ? key }) : super(key: key);
          @override
          State<AuthorPage> createState() => _AuthorPageState();
      }
      
    • Define a child _AuthorPageState class for showing the author data:

      class _AuthorPageState extends State<AuthorPage> {
      String name = "";
      String description = "";
      String avatar = "";
      
      @override
      Widget build(BuildContext context) {
          name = widget.name;
          description = widget.description;
          avatar = "https://images.quotable.dev/profile/400/${widget.slug}.jpg";
          return  Scaffold(
          appBar: AppBar(
              title: const Text('Author'),
          ),
          body:  CustomScrollView(
                  slivers: [
                  SliverToBoxAdapter(
                      child: Container(
                      height: MediaQuery.of(context).size.height,
                      padding: const EdgeInsets.all(20),
                      child: Column(
                          children: [
                          Column(
                          children: [
                              const Text("Name",style: TextStyle(fontWeight: FontWeight.bold),),
                              SizedBox(height:MediaQuery.of(context).size.height * 0.01),
                              Text(name)
                          ],
                          ),
                          SizedBox(height:MediaQuery.of(context).size.height * 0.02),
                          Column(
                          children: [
                              const Text("Description",style: TextStyle(fontWeight: FontWeight.bold)),
                              SizedBox(height:MediaQuery.of(context).size.height * 0.01),
                              Text(description)
                          ],
                          ),
                          SizedBox(height:MediaQuery.of(context).size.height * 0.02),
                          Column(
                          children: [
                              const Text("Avatar",style: TextStyle(fontWeight: FontWeight.bold)),
                              SizedBox(height:MediaQuery.of(context).size.height * 0.01),
                              Image.network(avatar)
                          ],
                          ),
                          SizedBox(height:MediaQuery.of(context).size.height * 0.02),
                          ],
                      )
                      )
                  )])
          );
      }
      }
      
  • In the lib/Screens/home_page.dart file:

    • Import the authors page file:

      import 'package:graphql_app/Screens/author_page.dart';
      
    • On the Post card, once the trailing icon navigate to the authors page:

      onPressed: () {// Navigate to the authors page
      Navigator.push(
      context,
          MaterialPageRoute(builder: (context) =>  AuthorPage(
              slug:data['slug'],
              name:data['name'],
              description:data['description'],
          )),
      );
      },
      
  • Ensure that the development server is running and click on any post. The resulting page should be similar to:

specific_page

Writing relevant test.

  • In the project directory, install the test dependency:

    flutter pub get test
    
  • In the lib directory, create a test.dart file.

  • test.dart:

    • Import the necessary packages:

      import 'dart:convert';
      
      import 'package:flutter/material.dart';
      import 'package:flutter_test/flutter_test.dart';
      import 'package:http/http.dart' as http;
      
    • Define the main function:

      void main(){
      
      }
      
    • In the main(), define a test widget that displays an author from the Author API:

      testWidgets('QuoteWidget displays an author from Author API',
          (WidgetTester tester) async {
          // Fetch data from the author API
          final response =
              await http.get(Uri.parse('https://quotable.io/authors'));
          final data = jsonDecode(response.body);
          final name = data['results'][0]['name'];
      
      
          // Build the QuoteWidget with the fetched data
          await tester.pumpWidget(
          MaterialApp(
              home: Scaffold(
              body: Text( name),
              ),
          ),
          );
      
          // Verify that the quote is displayed correctly
          expect(find.text(name), findsOneWidget);
      });
      
  • Run the test:

    flutter test lib/test.dart
    

Connecting the application on codemagic

  • To connect to codemagic, ensure you have published your code on GitHub.

  • Login or sign up on codemagic.

  • Once you have your account, the next step is to set up a build workflow for deployment.

Setting up the build workflow on codemagic

  • To continue, ensure you have connected your Google Play Store account with codemagic by following this guide.

  • To set up a build workflow, click on Create new workflow.

  • Select Android on the platforms.

  • On the Build Triggers section, select Trigger on push.

  • On the Build section, select the Flutter version and ensure you have selected the release mode.

  • On the Distribution, enable Android code signing, upload your keystore, enter the password and the alias.

  • On the second tab, enable Google Play Publishing, Upload the Credentials.json file, On the Track section, select Production.

  • Click on Save changes and then Start new build.

Deploying on codemagic to Google Play store

  • Once you do a push to the branch, a deployment will be started automatically.

build_on_codemagic

  • On your Playstore, the request will be received and queued for review:

play_store_release

Flutter CI/CD | Building, Code signing, and Publishing

Written By:

Joseph Chege