Building an Interactive Map in Flutter: An Epic Saga of What Went Wrong

Hello, everyone! Welcome to another post from my app development journey. This time, I’m sharing the story of all the trials and tribulations I faced while building what I thought was a simple idea: an on-device, interactive info app. What started as a simple concept turned into a multi-day debugging adventure. 😂

1. The 3D Dream and the First Ordeal: flutter_cube

The initial goal was to create a 3D globe that would display information when you tapped on a country. For this, I chose a package called flutter_cube.

  • Problem #1: The 3D model won’t show up!
    • Symptom: I followed the example code to display a basic cube, but I was immediately hit with a The asset does not exist error. I assumed that an asset included with the package would “just work.”
    • Failed Attempt: I tried adding the package’s asset path (packages/flutter_cube/assets/) directly to my pubspec.yaml, but that failed with a new unable to find directory error.
    • ✅ Solution: The most reliable solution was to manually copy the asset from the package into my own project. I navigated to Flutter’s .pub-cache directory, found the flutter_cube package, copied its entire assets/cube folder into my project’s assets/ folder, and declared - assets/cube/ in my pubspec.yaml. Finally, a gray cube appeared on the screen.
  • Problem #2: The Texture API Labyrinth
    • Symptom: The journey to apply a texture to that cube was a complete disaster. I tried every API call I could think of: textureFileNameobject.textureobject.material.textureobject.mesh.texture, and object.mesh.material.texture. Every single one failed.
    • ✅ Solution (or, giving up): After a long battle, I decided to stop fighting with the flutter_cube package and look for a more modern, easier-to-use alternative. Sometimes, switching your tools is the smartest choice.

2. A New Hope and Another Wall: model_viewer_plus

My next choice was model_viewer_plus, a package based on Google’s 3D viewer technology.

  • Problem #1: The method 'ModelViewer' isn't defined
    • ✅ Solution: This was a classic rookie mistake! I forgot to add the import 'package:model_viewer_plus/model_viewer_plus.dart'; statement at the top of my file. (Happens to the best of us, right? 😉)
  • Problem #2: Hitting a Conceptual Dead End
    • Symptom: I successfully displayed a beautiful, interactive .glb model of the Earth. But when I tried to implement the core feature—tapping on a specific country—I realized there was no way to do it.
    • ✅ Solution (A strategic pivot): I concluded that model_viewer_plus is, as the name implies, more of a “viewer” than a full 3D engine. It’s fantastic for showcasing models but not for complex interactions. I had to give up on the 3D dream for now and pivot the plan to a 2D interactive map.

3. The Final Destination: syncfusion_flutter_maps

I finally landed on syncfusion_flutter_maps, which has strong support for offline GeoJSON rendering and interactivity. This is where the last of my “struggles” began.

  • Problem #1: The map isn’t drawing! (The Black Screen of Death)
    • Symptom: The app compiled and ran without errors, but all I saw was a black screen.
    • Cause: The data I was trying to use for my world_map.json file was a plain JSON with country info, not a GeoJSON file with border coordinates. The package had no shape data to draw.
    • ✅ Solution: Using a correctly formatted GeoJSON file fixed the issue. Understanding the exact data format your tools expect is critical.
  • Problem #2: The Map Only Appears on Hot Reload!
    • Symptom: The app would show a blank screen on the initial launch, but the map would magically appear after a single hot reload.
    • Cause: This was a classic race condition. The Flutter app was trying to build the SfMaps widget before the world_map.json asset was fully loaded and ready on the native side. Hot reload worked because the asset was already available after the initial launch.
    • ✅ SolutionUsing a FutureBuilder was the right idea, but my first implementation was flawed. The final, correct solution was to use the FutureBuilder to first load the raw JSON string from the asset bundle into memory. Then, once the data is ready, the builder function creates the map source from that in-memory data using MapShapeSource.memory. This guarantees the data is fully loaded before the widget tries to render it.

Conclusion & Final Code

After a long journey, we finally have the foundation for our on-device, interactive 2D map app. This experience was a great reminder that sometimes the most important part of development is choosing the right tool for the job and understanding the low-level details of how your app loads data.

Here is the final, stable code that works on the first try:

// lib/main.dart (Final, working code)
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_maps/maps.dart';

// ... (MyApp class)

class GlobeScreen extends StatefulWidget {
  const GlobeScreen({super.key});

  @override
  State<GlobeScreen> createState() => _GlobeScreenState();
}

class _GlobeScreenState extends State<GlobeScreen> {
  // The Future now waits for the String content of the file
  late Future<String> _geoJsonData;

  @override
  void initState() {
    super.initState();
    // In initState, we just start the Future that reads the asset file
    _geoJsonData = rootBundle.loadString('assets/data/world_map.json');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Info Globe'),
      ),
      // The FutureBuilder now waits for String data
      body: FutureBuilder<String>(
        future: _geoJsonData,
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(child: Text('Error loading map: ${snapshot.error}'));
          }
          if (snapshot.hasData) {
            // Once the data is loaded, create the map source from memory
            final MapShapeSource mapSource = MapShapeSource.memory(
              // Convert the string data to bytes
              utf8.encode(snapshot.data!),
              shapeDataField: 'name',
            );

            return SfMaps(
              layers: <MapLayer>[
                MapShapeLayer(
                  source: mapSource, // Use the source created from memory
                  color: Colors.grey[350],
                  strokeColor: Colors.white,
                  strokeWidth: 0.5,
                  // We'll add selectionSettings and onSelectionChanged here later
                ),
              ],
            );
          }
          
          return const Center(child: Text('Loading map...'));
        },
      ),
    );
  }
}

Reference

https://heavenly.tistory.com/entry/Flutter-인터랙티브-지도-앱-만들기-삽질과-해결의-대서사시

댓글 남기기