← All posts

7 min read

Flutter for Android Developers: Mapping Kotlin and Java Concepts to Flutter

Flutter for Android developers: how Activities, Views, RecyclerView, ViewModel, resources and Intents map to Flutter, and where your Android skills still pay off.

  • Flutter
  • Android
  • Kotlin
Cover illustration for Flutter for Android Developers: Mapping Kotlin and Java Concepts to Flutter

If you’ve built Android apps in Kotlin or Java, your first hour in Flutter is confusing in a specific way. You go looking for the Activity, the layout XML, the adapter, and they aren’t there. Then you open the android/ folder and find Gradle, a manifest and a MainActivity, and it feels like home again, just smaller.

I write native Android in Kotlin and Java as well as Flutter, and I once rewrote an entire native app in Flutter at Shiftboard. So I’ve made this transition myself. This post is the guide I wish I’d had: what maps to what, what genuinely changes, and where your Android experience is still worth a lot.

The quick translation table

Start here, then read the sections below for the parts that aren’t one-to-one.

Android Flutter
Activity / Fragment Routes and screens (widgets), usually managed by a router
View / ViewGroup Widget
XML layouts Widget trees written in Dart
RecyclerView + Adapter ListView.builder / GridView.builder
ViewModel + LiveData / StateFlow Notifier, Cubit or ChangeNotifier (state holders)
res/drawable, res/values/strings.xml assets/ in pubspec, ARB files for localization
Intents to other apps Plugins (e.g. url_launcher), or platform channels
Lifecycle callbacks State lifecycle, AppLifecycleListener
Gradle Still Gradle, for the Android side of the build

Activities and Fragments become routes and widgets

A Flutter app on Android runs inside a single Activity. Look at android/app/src/main/kotlin/.../MainActivity.kt and you’ll find a MainActivity that extends FlutterActivity and usually does nothing else. Everything the user sees is drawn by Flutter inside that one Activity.

So there’s no Activity per screen and no Fragment transactions. A screen is just a widget, and moving between screens is navigation between routes. You can use the built-in Navigator, but most apps I build use a router package like go_router, which gives you URL-style paths and deep linking. Think of it as a Navigation Component without the XML graph.

The good news: no more Fragment lifecycle bugs, no back stack surprises from commit() versus commitNow(), and configuration changes like rotation don’t destroy your screen. Flutter just rebuilds the widgets with the new size.

Views become widgets, and widgets are cheap

This is the biggest mental shift. In Android, a View is a long-lived object. You inflate it once, hold a reference, and mutate it: textView.setText(...), button.setEnabled(false).

In Flutter you never hold a reference to a widget to change it. Widgets are lightweight, immutable descriptions of the UI. When state changes, you build a new description and Flutter works out what actually changed on screen.

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

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text('Tapped $_count times'),
        FilledButton(
          onPressed: _count >= 10 ? null : () => setState(() => _count++),
          child: const Text('Tap'),
        ),
      ],
    );
  }
}

There’s no findViewById, no binding, no “update the text view”. The UI is a function of the state. When the button should be disabled, you don’t disable it. You describe a button whose onPressed is null. If you’ve used Jetpack Compose, this will feel familiar, because Compose is built on the same declarative idea. If you’ve only used XML views, give it a week. Once it clicks, going back feels like a lot of bookkeeping.

RecyclerView becomes ListView.builder

Remember writing an Adapter, a ViewHolder, onCreateViewHolder, onBindViewHolder and a DiffUtil callback? In Flutter, a lazily built, recycled list is this:

ListView.builder(
  itemCount: orders.length,
  itemBuilder: (context, index) {
    final order = orders[index];
    return ListTile(
      title: Text(order.customerName),
      subtitle: Text(order.status),
    );
  },
)

Flutter builds only the items that are visible, just like RecyclerView. There’s no ViewHolder because widgets are cheap to recreate, and no DiffUtil because the framework compares the widget trees itself. For grids, use GridView.builder. For complex scrolling layouts with collapsing headers, Flutter has slivers, which are roughly what CoordinatorLayout wanted to be.

One thing to watch: if items can be reordered, removed or inserted, give them stable keys, such as a ValueKey(order.id). That’s how Flutter knows which item is which, much like stable IDs in a RecyclerView adapter.

ViewModels become state holders

The Android pattern of a ViewModel exposing a StateFlow that the UI observes maps neatly onto Flutter. The ViewModel becomes a Riverpod Notifier, a BLoC Cubit, or a plain ChangeNotifier. The UI watches it and rebuilds when it changes. Your repositories, use cases and the general clean architecture layering carry over almost unchanged.

What you don’t need is the ViewModel’s main job on Android: surviving configuration changes. Flutter doesn’t recreate your screen on rotation, so state holders are about separating logic from UI, not about outliving the Activity.

Resources become assets and ARB files

There’s no R class. Images, fonts and other files go in an assets/ folder and get listed in pubspec.yaml. Instead of drawable-hdpi and friends, you provide 2.0x and 3.0x folders next to the base image, and Flutter picks the right one.

Strings move from strings.xml to ARB files, which are JSON files with one per language. Flutter’s localization tooling generates a typed Dart class from them, so AppLocalizations.of(context).welcomeTitle replaces getString(R.string.welcome_title), with the same compile-time safety. Colors and dimensions usually move into a ThemeData, which plays the role of your themes.xml and colors.xml.

Intents become plugins and platform channels

Opening a URL, dialing a number, sharing text or opening the camera all used to be Intents. In Flutter, each of these is a plugin: url_launcher, share_plus, image_picker and so on. Under the hood, those plugins are Kotlin or Java code firing the same Intents you would have written.

When no plugin does what you need, you write that native code yourself and call it from Dart through a platform channel. Platform channels and Pigeon covers how I do that with type safety. This is where Android developers have a real advantage. A lot of Flutter developers get nervous when they have to open the android/ folder. You won’t.

Hot reload changes how you work

Hot reload deserves its own section because it changes your daily rhythm more than any API does. You edit a widget, save, and the running app updates in about a second, keeping its current state. You’re still on the same screen with the same form input, now with the padding fixed.

No rebuilding, reinstalling or tapping back to the screen you were testing. Two things to know: changes to main(), global initializers or initState logic need a hot restart, which resets state but is still fast, and changes to native code or Gradle files need a full rebuild.

Where your Android knowledge still pays off

Flutter abstracts away a lot of Android, but not all of it. These are the places where I see Android developers outperform people who only know Flutter:

  • Gradle. The Android build is still Gradle, now often with Kotlin DSL files. Signing configs, flavors, minSdk and targetSdk, dependency conflicts from plugins, and R8/ProGuard rules all live there. When a plugin breaks the build, you can read the error.
  • The manifest and permissions. Permissions, intent filters for deep links, exported components and foreground service declarations still go in AndroidManifest.xml. You already know runtime permissions and why the system dialog sometimes won’t show.
  • Native crashes. When a crash comes from inside a plugin’s Kotlin code or from native libraries, the Dart stack trace won’t help. Reading Logcat output, a native stack trace or an ANR report is a skill most Flutter-only developers don’t have.
  • Platform behavior. Background execution limits, battery optimizations, notification channels and Play Store policies are Android topics, whatever framework draws your UI.
  • Writing native code when you need it. Some features are better done natively. I cover when that’s worth it in when to write native code in a Flutter app.

How I’d learn it

Skip the urge to recreate Android architecture in Flutter on day one. Build one small, real app: a list, a detail screen, a form and an API call. Use setState first, then move the logic into a state holder once you feel why you need one. Learn Dart’s null safety, async/await and records along the way. Most of it will feel like a lighter Kotlin.

If you’re weighing a full move of an existing native app to Flutter, that’s a bigger decision with its own trade-offs. I wrote about it in rewriting a native app in Flutter.

Takeaways

  • A Flutter app runs in one FlutterActivity. Screens are widgets, and navigation is between routes.
  • Widgets are immutable descriptions you rebuild, not views you mutate. If you know Compose, you’re halfway there.
  • ListView.builder replaces RecyclerView, adapters and DiffUtil, and stable keys replace stable IDs.
  • ViewModels map to Notifiers or Cubits, minus the configuration-change survival job.
  • Gradle, the manifest, permissions and native debugging are still part of the job, and that’s where Android experience gives you an edge.

If you’re an Android team considering Flutter for your next app and want help planning the move, get in touch.

Comments

Questions, corrections or your own experience — leave a comment below (GitHub sign-in).