How good is the development solution? / Hebrew

How good is the development solution? / Hebrew

Popularity of telegrams

Which messenger do you use most often now? Most will respond to Telegram, and those who say otherwise are still likely to use it somewhere, even if less often. It’s no secret that Telegram is one of the most popular messengers in the world. As befits a rapidly developing product, it later began to add unique features, one of which was its own built-in browser (relevant for mobile devices). But it should be noted that Mini Apps appeared in Telegram before that.

Based on the name, you can guess that these are applications, but not quite in the usual way. WebApps, as they were originally called, are a special case of applications, they are web programs. As any site should, they are created using the standard web technology stack: HTML, CSS and JavaScript, and look accordingly.

One day I thought, since I’m a cross-platform developer on Flutter, how well would it perform on the web? Namely, how convenient will it be to create and use such a product? After all, you don’t need to reinvent the wheel to write code on this framework — you write the same code and be happy that it works. But I knew this only in theory, and how it would work in practice, I had to find out.

Where did the idea of ​​creating a web program come from?

All events took place in the summer of 2024. So, Mini Apps is a relatively new feature of Telegram and there haven’t been many implemented ideas in the market yet. This meant: “Whoever had time, ate.” There was absolutely no time to selectively gather a team, so I called good friends and rushed. Immediately, without wasting a single minute of time, at the very first bell we impromptu decided the question: what would we like to create? There were a lot of ideas, but many factors had to be taken into account. One of them was organic growth. It was critical for us that our product evolve without our intervention, except for the development part. Based on this, we needed to invent something true.

I didn’t have to think particularly: I myself, and my friends, sometimes like to fail in all kinds of tests, being surprised by what a mixer you are. And then we realized that this is what we need. We decided to create our improved version of tests with the prospect of introducing monetization for the most popular creators. Everyone was in favor, and that was enough to cover the original idea of ​​testing Flutter in mini-apps.

To implement this idea, it was necessary to immediately define the functions and, based on them, select those that would be sufficient for the MVP. In total, I highlighted three features that I would like to see in the application:

  • Take pre-prepared tests;

  • share links to passed tests;

  • Create user tests;

  • Monetize your trials;

All of them are very good, but it is easy to guess that the last two will somehow be more laborious compared to the first, so we limited ourselves to one function for the initial version. The mother part was finished, and creation could finally begin.

Realization

We will need a bot to interact with our web application. I think there is no point in explaining how to create it, so let’s go straight to Telegam Mini App. In order to create it, you need to understand what it is. By and large, this is a WebView that opens inside the Telegram shell via a link. That is, to connect all this, we need a working site, so for now we will wait with the bot and leave it for last.

Quick design and layout in figma

As with any application, we needed a design, at least some. Therefore, we quickly sketched out a mock-up of how it would all look in Figma, and started the layout. As already mentioned, the codebase does not change for this framework, and if you have already written on Flutter, then there will be no problems. And so it turned out: on the web, all widgets and other details looked the same as on mobile phones. Therefore, one could not forget about adaptability. Although our app didn’t really need a lot of responsiveness due to the already responsive widgets, we still had to play around with MediaQuery a bit.

Development of logic and work with the backend

Since we didn’t want to bother much with the backend, we chose PocketBase as SaaS. It completely satisfied our requirements of just saving the tests. The architecture of the tests was quite simple:

  1. Test ID

  2. Home page (contains a picture of the test, description, views)

  3. Test pages (contain questions and a picture for the test)

  4. Results pages (contains possible pages of results, which in turn show the most frequently chosen number, picture and description)

To beat this logic, I had to think a little. The primary goal was to keep the user experience as pleasant as possible, so passing the tests should be intuitive. For this we used PreloadPageControllerwhich, upon opening the test, immediately loaded all the pages that will be used later. We did not have a quiz entity as such, and all data was stored in three status fields:

final class QuizLoaded extends QuizState {
  final StartEntity startPage;
  final List finalPage;
  final List pages;
  final Map answers;

  @override
  List

Yes, maybe this is not the best option, but at that moment it seemed the most suitable for us. All the code looked something like this for the tests:

else if (state is QuizLoaded) {
              final startEntity = state.props[0] as StartEntity;
              final finalEntites =
                  state.props[1] as List;
              final pageEntities =
                  state.props[2] as List;
              return PreloadPageView(
                controller: pageController,
                onPageChanged: (index) {
                  if (index - 1 == pageEntities.length) {
                    context.read().add(QuizCompletedEvent());
                  }
                },
                children: _buildPages(
                  context: context,
                  startEntity: startEntity,
                  pageEntities: pageEntities,
                  pageController: pageController,
                  finalEntities: finalEntites,
                  answers: state.answers,
                ),
              );
            } else if (state is QuizCompleted) {
              final finalPage = state.finalpage;
              return FinalPageQuiz(
                quizId: widget.id,
                finalId: finalPage.id,
                image: finalPage.image,
                name: finalPage.name,
                description: finalPage.description,
                mostFrequentDigit: finalPage.mostFrequentDigit,
              );
            }
            return Container();
          },
        ),
      );


  List _buildPages({
    required BuildContext context,
    required StartEntity startEntity,
    required List pageEntities,
    required PreloadPageController pageController,
    required List finalEntities,
    required Map answers,
  }) {
    final pages = [];

    pages.add(
      FirstPageQuiz(
        id: startEntity.id,
        description: startEntity.description,
        image: startEntity.image,
        name: startEntity.name,
        pageController: pageController,
      ),
    );

    for (var i = 0; i ,
          pageController: pageController,
          id: widget.id,
        ),
      );
    }

    return pages;
  }

Now I’ll put everything on the shelves in this big piece of code. The main point is the build of all test pages and the formation of the last page (i-1). Even the function that returns the widget is considered a movetone, but here it fit perfectly.

After the user enters the test, it loads all the data, which triggers the event that emits QuizLoadedcontaining all required fields. Then the most interesting begins: PreloadPageView calls a function _buildPageswhich returns all the screens available to the user before the result is calculated, i.e. the start page and the test page. After downloading, the user can see the entire test.

When the user selects an answer, it sends an event with the answer map changed, where the key is the question number and the value is the answer option. At the same time, for each page change, the length of all test pages is checked, and if the next page is the penultimate one, an event is sent to calculate the last page, which emits the corresponding state.

else if (state is QuizCompleted) {
              final finalPage = state.finalpage;
              return FinalPageQuiz(
                quizId: widget.id,
                finalId: finalPage.id,
                image: finalPage.image,
                name: finalPage.name,
                description: finalPage.description,
                mostFrequentDigit: finalPage.mostFrequentDigit,
              );
            }

And how it does it now, let’s figure it out.

  Future _generateFinalScreen(
    QuizCompletedEvent event,
    Emitter emit,
  ) async {
    if (state is QuizLoaded) {
      final loadedState = state as QuizLoaded;
      final answers = loadedState.answers;
      final finalsEntities = loadedState.finalPage;

      final finalPageEntity = _determineFinalPage(
        finalEntities: finalsEntities,
        answers: answers,
      );
      emit(
        QuizCompleted(
          finalpage: finalPageEntity,
        ),
      );
    }
  }

As you can see here, half of the data is taken from the state, it is not surprising, these are the answers and the essence of the final pages, but what a mystery _determineFinalPage?

FinalEntity _determineFinalPage({
    required List finalEntities,
    required Map answers,
  }) {
    final valueCounts = {};

    for (final value in answers.values) {
      if (valueCounts.containsKey(value)) {
        valueCounts[value] = valueCounts[value]! + 1;
      } else {
        valueCounts[value] = 1;
      }
    }
    var mostFrequentValue = valueCounts.keys.first;
    var maxCount = valueCounts[mostFrequentValue]!;

    valueCounts.forEach((key, count) {
      if (count > maxCount) {
        mostFrequentValue = key;
        maxCount = count;
      }
    });

    final finalPage = finalEntities.firstWhere(
      (finalpage) {
        if (finalpage.mostFrequentDigit == mostFrequentValue - 1) {
          return true;
        }

        return false;
      },
      orElse: () => finalEntities.first,
    );
    return finalPage;
  }

I agree, the feature may look a little confusing, but we had a specific goal. She accepts the list of final pages and the answer card. And it works according to the following logic:

  1. Considers answers: First, the function counts how many times each response value of the answer occurs (from 0 to 3).

  2. The most common values ​​are: Then the function determines the most common among them.

  3. Search for the Final Page: And finally, in the final pages, he looks for the first element that has mostFrequentDigit is equal to mostFrequentValue - 1.

PS Value mostFrequentDigit - 1 related to a feature, rather than a bug that occurred during the layout. All answers were shifted by -1, that is, from 0 to 3. Since the logic was already built on this option, it was decided to leave it in this form.

Development of Deep Links

From the very beginning of the development of tests, we sought possible organic growth, so it was necessary to create maximum convenience for users. This included the ability to create and navigate to your own tests. But how can you redirect users to the desired test without a link ID? Answer: No way.

Initially, we planned to create an addressing capability for tests. However, since we temporarily postponed this function, it was decided to change the purpose of deeplinks to the ability to share tests with friends that have already been passed. The logic itself looked something like this:

A block diagram of how the navigation processing logic worked for a specific test

To implement interaction with the Telegram API, we used the telegram_web_app package, which at the time of development was at version 0.1.0. The link to the test itself looked like this:

https://t.me//base?startapp={***args***}

Where args – would be the identifier we would need. Since Telegram immediately opened our program, it was decided to redirect any routes with the possibility of checking to the starting parameters. The entire piece of code looked like this:

redirect: (BuildContext context, GoRouterState state) async {
  final initData = TelegramWebApp.instance.initDataUnsafe;

  if (initData?.startParam != null) {
    final path="/quiz/${initData!.startParam}";
    return path;
  }

  if (state.matchedLocation.contains('/quiz')) {
    return null;
  }

  return '/';
},

Literally, when a person opens the program, the router checks whether the user has startup parameters startapp. If the parameters are present, it redirects the user to the page with the required test. If there are no startup parameters, the router returns the root path with all tests. And it worked! A person clicked on a link and opened the test he needed, with data uploaded from a remote database.

Problems with scrolling

The indication of the version of the package was not accidental. The fact is that in that version of Telegram Mini Apps there was a common bug: “A bug with collapsing when scrolling in Web App for Telegram Bot”. This bug meant that if the user tried to scroll through the tests, the program could unwittingly crash, creating significant gaps for our tests to function. In this regard, we had to edit the native code of the HTML document and add our own JavaScript script that prevented the window from collapsing when scrolling down. However, now, starting with API version 7.7, a very convenient method has appeared disableVerticalSwipeswhich completely eliminated this problem.

Site deployment for hosting and bot settings

So let’s go back to the first part when we created the bot. After our web application is ready, it needs to be placed on the server and connected to any hosting so that it is available in the browser using the HTTPS protocol. After all these manipulations, you will have a link to your program that you are copying. Next, go to the bot and go to the settings:

Click on Configure Mini Appafter which activate Enable Mini App. The next step is to send a link to the site and go to check the bot.

If you did everything according to the instructions, you will see a button in the description of the bot Open Appclicking on which will launch your application.

Result

As a result, even on a small project, Flutter for Telegram Mini Apps showed itself from the best side. Despite the fact that the project had to be closed due to an incomplete marketing strategy, we achieved our equally important goal – to check how Flutter will cope with the task for Telegram Mini Apps. The app worked smoothly and quickly, and the technology turned out to be easier than expected. If you have any questions or you have also tried to make mini-applications – share your experience in the comments, I will be happy to chat! And for those who are looking for more such material and do not mind looking behind the scenes of new articles and my personal insights – sign up for a visa!

Related posts