{"id":4105,"date":"2025-12-12T06:22:00","date_gmt":"2025-12-12T06:22:00","guid":{"rendered":"https:\/\/alsaeeddev.com\/admin-wordpress\/?p=4105"},"modified":"2025-12-12T07:37:19","modified_gmt":"2025-12-12T07:37:19","slug":"custom-loading-animation-flutter-jetpack-compose","status":"publish","type":"post","link":"https:\/\/alsaeeddev.com\/shop\/custom-loading-animation-flutter-jetpack-compose\/","title":{"rendered":"How to Use Jetpack Compose and Flutter to Make a Custom Loading Animation for Mobile App"},"content":{"rendered":"<p>Loading indicators are an important part of making a mobile experience look and feel smooth and professional. A lot of developers use built-in progress bars or spinners, but custom animations can make your app look different, help with branding, and make it feel like it&#8217;s flowing.<\/p>\n<p>This guide will show you how to use Jetpack Compose (for Android) and Flutter to make a more complex custom loading animation for mobile apps. The goal is to make a fun-to-watch multi-ring spinner animation that spins in different directions, at different speeds, and with arcs of different shapes. Your UI will look fresh and new with this.<\/p>\n<h2>\u2b50 Why It&#8217;s Important to Have a Custom Loading Animation<\/h2>\n<p>A good loading animation will give you<\/p>\n<ul>\n<li>More interaction with users<\/li>\n<li>Different looks for each brand<\/li>\n<li>There are no issues when you switch screens.<\/li>\n<li>It feels like a premium app.<\/li>\n<\/ul>\n<p>People believe they don&#8217;t have to wait as long.<\/p>\n<p>You can make complicated animated parts with very little code using <strong>Jetpack Compose<\/strong> and <strong>Flutter<\/strong>, two modern UI frameworks.<\/p>\n<h3>Jetpack Compose for Android lets you make your own loading animation.<\/h3>\n<p>Developers can make UI in a declarative way with Jetpack Compose and use powerful APIs to add animations. We want to make a spinner with a lot of rings that are stacked on top of each other and spin by themselves.<\/p>\n<h5 data-start=\"1662\" data-end=\"1695\">1. Model for Setting Up a Ring<\/h5>\n<p data-start=\"1697\" data-end=\"1750\">First, we discuss the features of each animated ring:<\/p>\n<ul data-start=\"1752\" data-end=\"1861\">\n<li data-start=\"1752\" data-end=\"1762\">\n<p data-start=\"1754\" data-end=\"1762\">Radius<\/p>\n<\/li>\n<li data-start=\"1752\" data-end=\"1762\">\n<p data-start=\"1754\" data-end=\"1762\">Color<\/p>\n<\/li>\n<li data-start=\"1773\" data-end=\"1789\">\n<p data-start=\"1775\" data-end=\"1789\">Stroke width<\/p>\n<\/li>\n<li data-start=\"1790\" data-end=\"1801\">\n<p data-start=\"1792\" data-end=\"1801\">Opacity<\/p>\n<\/li>\n<li data-start=\"1802\" data-end=\"1816\">\n<p data-start=\"1804\" data-end=\"1816\">Arc length<\/p>\n<\/li>\n<li data-start=\"1817\" data-end=\"1842\">\n<p data-start=\"1819\" data-end=\"1842\">Direction of rotation<\/p>\n<\/li>\n<li data-start=\"1843\" data-end=\"1861\">\n<p data-start=\"1845\" data-end=\"1861\">Rotation speed<\/p>\n<\/li>\n<\/ul>\n<p data-start=\"1863\" data-end=\"1934\">These properties allow full customization for each ring in the spinner.<\/p>\n<p data-start=\"1863\" data-end=\"1934\"><strong>RingConfig Model<\/strong><\/p>\n<div>\n<pre><code class=\"language-java\"> \r\ndata class RingConfig(\r\n    val id: String,\r\n    val radius: Float,\r\n    val color: Color,\r\n    val strokeWidth: Float,\r\n    val opacity: Float,\r\n    val arcLength: Float,\r\n    val direction: Int,     \/\/ +1 or -1\r\n    val speed: Float        \/\/ seconds per rotation\r\n)\r\n <\/code><\/pre>\n<\/div>\n<h5>2. Creating the Custom Loading Animation<\/h5>\n<p>For smooth animations that never end, Compose has <strong>rememberInfiniteTransition<\/strong>.<\/p>\n<p>Each ring has its own float animation that shows how it turns:<\/p>\n<ul>\n<li data-start=\"2139\" data-end=\"2177\">\n<p data-start=\"2141\" data-end=\"2177\">Independent rotation for each ring<\/p>\n<\/li>\n<li data-start=\"2178\" data-end=\"2214\">\n<p data-start=\"2180\" data-end=\"2214\">Control over speed and direction<\/p>\n<\/li>\n<li data-start=\"2215\" data-end=\"2247\">\n<p data-start=\"2217\" data-end=\"2247\">Smooth, continuous animation<\/p>\n<\/li>\n<\/ul>\n<h5>3. Drawing on a Canvas<\/h5>\n<p>There are dashed arcs and a rotating effect on the rings drawn on Canvas.<\/p>\n<p>This setup allows:<\/p>\n<ul>\n<li>Changeable radius<\/li>\n<li>Stroke width that can be changed<\/li>\n<li>Dividing arcs into parts<\/li>\n<li>Each layer can move by itself.<\/li>\n<\/ul>\n<p>These pieces work together to make a loading animation that looks great and works well with Android apps that are up to date.<\/p>\n<p><strong style=\"font-size: 16px;\">Spinner Composable Function<\/strong><\/p>\n<div>\n<pre><code class=\"language-java\">\r\n\r\n@Composable\r\nfun Loading(\r\n    rings: List,\r\n    size: Dp = 400.dp\r\n) {\r\n    \/\/  All animations must be created here, NOT inside Canvas\r\n    val transition = rememberInfiniteTransition()\r\n\r\n    \/\/ Pre-calc animations for each ring\r\n    val rotations = rings.map { ring -&gt;\r\n        transition.animateFloat(\r\n            initialValue = 0f,\r\n            targetValue = ring.direction * 360f,\r\n            animationSpec = infiniteRepeatable(\r\n                animation = tween(\r\n                    durationMillis = (ring.speed * 1000).toInt(),\r\n                    easing = LinearEasing\r\n                ),\r\n                repeatMode = RepeatMode.Restart\r\n            )\r\n        )\r\n    }\r\n\r\n    Box(\r\n        modifier = Modifier.size(size),\r\n    ) {\r\n        Canvas(modifier = Modifier.fillMaxSize()) {\r\n\r\n            val center = Offset(this.size.width \/ 2, this.size.height \/ 2)\r\n\r\n            rings.forEachIndexed { index, ring -&gt;\r\n                val rotation = rotations[index].value\r\n\r\n                val circumference = 2f * PI.toFloat() * ring.radius\r\n                val dashArray = circumference * ring.arcLength\r\n                val dashGap = circumference - dashArray\r\n\r\n                rotate(rotation, pivot = center) {\r\n                    drawCircle(\r\n                        color = ring.color.copy(alpha = ring.opacity),\r\n                        radius = ring.radius,\r\n                        center = center,\r\n                        style = Stroke(\r\n                            width = ring.strokeWidth,\r\n                            cap = StrokeCap.Round,\r\n                            pathEffect = PathEffect.dashPathEffect(\r\n                                floatArrayOf(dashArray, dashGap),\r\n                                phase = 0f\r\n                            )\r\n                        )\r\n                    )\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n<\/code><\/pre>\n<\/div>\n<p>&nbsp;<\/p>\n<h3>How to use this Loading Composable Function<\/h3>\n<p><strong>MainActivity.kt<\/strong><\/p>\n<pre><code class=\"language-java\">     \r\nclass MainActivity : ComponentActivity() {\r\n    override fun onCreate(savedInstanceState: Bundle?) {\r\n        super.onCreate(savedInstanceState)\r\n        enableEdgeToEdge()\r\n        setContent {\r\n            LoaderTheme {\r\n                CustomSpinLoading()\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n@Composable\r\nfun CustomSpinLoading() {\r\n    val rings = listOf(\r\n        RingConfig(\r\n            id = \"a\",\r\n            radius = 50f,\r\n            color = Color.Black,\r\n            strokeWidth = 14f,\r\n            opacity = 1f,\r\n            arcLength = 0.6f,\r\n            direction = -1,\r\n            speed = 6f\r\n        ),\r\n        RingConfig(\r\n            id = \"b\",\r\n            radius = 80f,\r\n            color = Color.Red,\r\n            strokeWidth = 14f,\r\n            opacity = 1f,\r\n            arcLength = 0.8f,\r\n            direction = 1,\r\n            speed = 8f\r\n        ),\r\n        RingConfig(\r\n            id = \"c\",\r\n            radius = 120f,\r\n            color = Color.Blue,\r\n            strokeWidth = 14f,\r\n            opacity = 0.9f,\r\n            arcLength = 0.6f,\r\n            direction = -1,\r\n            speed = 9f\r\n        ),\r\n        RingConfig(\r\n            id = \"d\",\r\n            radius = 160f,\r\n            color = Color.Green,\r\n            strokeWidth = 14f,\r\n            opacity = 0.8f,\r\n            arcLength = 0.5f,\r\n            direction = 1,\r\n            speed = 9f\r\n        ),\r\n\r\n        RingConfig(\r\n            id = \"e\",\r\n            radius = 200f,\r\n            color = Color.Cyan,\r\n            strokeWidth = 14f,\r\n            opacity = 0.8f,\r\n            arcLength = 0.4f,\r\n            direction = -1,\r\n            speed = 8f\r\n        )\r\n    )\r\n\r\n    Loading(rings = rings, size = 400.dp)\r\n}\r\n  <\/code><\/pre>\n<hr \/>\n<p>&nbsp;<\/p>\n<h3>\ud83d\udfe9 Flutter Loading Animation That You Can Change<\/h3>\n<p>Flutter has its own animation system that is based on controllers and custom painters, but it works in a similar way to declarative programming.<\/p>\n<p>Check out the code below:<\/p>\n<p><strong>RingConfig.dart <\/strong>Model Class<\/p>\n<pre><code class=\"language-java\"> \r\nclass RingConfig {\r\nfinal String id;\r\nfinal double radius;\r\nfinal Color color;\r\nfinal double strokeWidth;\r\nfinal double opacity;\r\nfinal double arcLength;\r\nfinal int direction; \/\/ +1 or -1\r\nfinal double speed; \/\/ seconds per rotation\r\n\r\nRingConfig({\r\nrequired this.id,\r\nrequired this.radius,\r\nrequired this.color,\r\nrequired this.strokeWidth,\r\nrequired this.opacity,\r\nrequired this.arcLength,\r\nrequired this.direction,\r\nrequired this.speed,\r\n});\r\n}\r\n<\/code><\/pre>\n<p>&nbsp;<br \/>\n<strong>Drawing with CustomPainter<\/strong><\/p>\n<pre><code class=\"language-java\"> \r\nimport 'dart:math';\r\nimport 'package:flutter\/cupertino.dart';\r\nimport 'RingConfig.dart';\r\n\r\nclass LoadingPainter extends CustomPainter {\r\nfinal List&lt;RingConfig&gt; rings;\r\nfinal List&lt;Animation&lt;double&gt;&gt; rotations;\r\n\r\nLoadingPainter(this.rings, this.rotations);\r\n\r\n@override\r\nvoid paint(Canvas canvas, Size size) {\r\nfinal center = Offset(size.width \/ 2, size.height \/ 2);\r\n\r\nfor (int i = 0; i &lt; rings.length; i++) {\r\nfinal ring = rings[i];\r\nfinal rotation = rotations[i].value;\r\n\r\nfinal paint = Paint()\r\n..color = ring.color.withValues()\r\n..style = PaintingStyle.stroke\r\n..strokeWidth = ring.strokeWidth\r\n..strokeCap = StrokeCap.round;\r\n\r\nfinal circumference = 2 * pi * ring.radius;\r\nfinal dashLength = circumference * ring.arcLength;\r\nfinal dashGap = circumference - dashLength;\r\n\r\nfinal circlePath = Path()\r\n..addOval(Rect.fromCircle(center: center, radius: ring.radius));\r\n\r\nfinal dashedPath = _dashPath(circlePath, dashLength, dashGap);\r\n\r\ncanvas.save();\r\ncanvas.translate(center.dx, center.dy);\r\ncanvas.rotate(rotation * pi \/ 180);\r\ncanvas.translate(-center.dx, -center.dy);\r\n\r\ncanvas.drawPath(dashedPath, paint);\r\n\r\ncanvas.restore();\r\n}\r\n}\r\n\r\n@override\r\nbool shouldRepaint(covariant CustomPainter oldDelegate) =&gt; true;\r\n}\r\n\r\nPath _dashPath(Path source, double dashLength, double dashGap) {\r\nfinal Path dest = Path();\r\ndouble distance = 0.0;\r\n\r\nfor (final metric in source.computeMetrics()) {\r\nwhile (distance &lt; metric.length) {\r\nfinal next = distance + dashLength;\r\ndest.addPath(metric.extractPath(distance, next), Offset.zero);\r\ndistance = next + dashGap;\r\n}\r\ndistance = 0.0;\r\n}\r\nreturn dest;\r\n}\r\n<\/code><\/pre>\n<p>&nbsp;<br \/>\n<strong>Custom Loading Widget<\/strong><\/p>\n<pre><code class=\"language-java\">\r\nimport 'package:flutter\/cupertino.dart';\r\nimport 'RingConfig.dart';\r\nimport 'LoadingPainter.dart';\r\nimport 'package:flutter\/material.dart';\r\n\r\n\r\nclass Loading extends StatefulWidget {\r\n  final List&lt;RingConfig&gt; rings;\r\n  final double size;\r\n\r\n  const Loading({super.key, required this.rings, this.size = 400});\r\n\r\n  @override\r\n  State&lt;Loading&gt; createState() =&gt; _LoadingState();\r\n}\r\n\r\nclass _LoadingState extends State&lt;Loading&gt; with TickerProviderStateMixin {\r\n  late List&lt;AnimationController&gt; controllers;\r\n  late List&lt;Animation&lt;double&gt;&gt; rotations;\r\n\r\n  @override\r\n  void initState() {\r\n    super.initState();\r\n\r\n    controllers = widget.rings.map((ring) {\r\n      return AnimationController(\r\n        duration: Duration(milliseconds: (ring.speed * 1000).toInt()),\r\n        vsync: this,\r\n      )..repeat();\r\n    }).toList();\r\n\r\n    rotations = List.generate(\r\n      widget.rings.length,\r\n      (i) =&gt; Tween&lt;double&gt;(\r\n        begin: 0,\r\n        end: widget.rings[i].direction * 360,\r\n      ).animate(controllers[i]),\r\n    );\r\n  }\r\n\r\n  @override\r\n  void dispose() {\r\n    for (var controller in controllers) {\r\n      controller.dispose();\r\n    }\r\n    super.dispose();\r\n  }\r\n\r\n  @override\r\n  Widget build(BuildContext context) {\r\n    return SizedBox(\r\n      width: widget.size,\r\n      height: widget.size,\r\n      child: AnimatedBuilder(\r\n        animation: Listenable.merge(controllers),\r\n        builder: (_, __) {\r\n          return CustomPaint(painter: LoadingPainter(widget.rings, rotations));\r\n        },\r\n      ),\r\n    );\r\n  }\r\n}\r\n<\/code><\/pre>\n<p>&nbsp;<\/p>\n<h3>How to use this Loading widget in Flutter?<\/h3>\n<p><strong>main.dart<\/strong><\/p>\n<pre><code class=\"language-java\">\r\nimport 'package:flutter\/material.dart';\r\n\r\nimport 'RingConfig.dart';\r\nimport 'Loading.dart';\r\n\r\nvoid main() {\r\n  runApp(const MyApp());\r\n}\r\n\r\nclass MyApp extends StatelessWidget {\r\n  const MyApp({super.key});\r\n\r\n  \/\/ This widget is the root of your application.\r\n  @override\r\n  Widget build(BuildContext context) {\r\n    return MaterialApp(\r\n      title: 'Custom Loading',\r\n      theme: ThemeData(\r\n        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),\r\n      ),\r\n      home: const MyHomePage(title: 'Loading Animation'),\r\n    );\r\n  }\r\n}\r\n\r\nclass MyHomePage extends StatefulWidget {\r\n  const MyHomePage({super.key, required this.title});\r\n\r\n  final String title;\r\n\r\n  @override\r\n  State&lt;MyHomePage&gt; createState() =&gt; _MyHomePageState();\r\n}\r\n\r\nclass _MyHomePageState extends State&lt;MyHomePage&gt; {\r\n  @override\r\n  Widget build(BuildContext context) {\r\n    final rings = [\r\n      RingConfig(\r\n        id: \"a\",\r\n        radius: 25,\r\n        color: Colors.black,\r\n        strokeWidth: 8,\r\n        opacity: 1,\r\n        arcLength: 0.6,\r\n        direction: -1,\r\n        speed: 6,\r\n      ),\r\n      RingConfig(\r\n        id: \"b\",\r\n        radius: 40,\r\n        color: Colors.red,\r\n        strokeWidth: 8,\r\n        opacity: 1,\r\n        arcLength: 0.8,\r\n        direction: 1,\r\n        speed: 8,\r\n      ),\r\n      RingConfig(\r\n        id: \"c\",\r\n        radius: 60,\r\n        color: Colors.blue,\r\n        strokeWidth: 8,\r\n        opacity: 0.9,\r\n        arcLength: 0.6,\r\n        direction: -1,\r\n        speed: 9,\r\n      ),\r\n      RingConfig(\r\n        id: \"d\",\r\n        radius: 80,\r\n        color: Colors.green,\r\n        strokeWidth: 8,\r\n        opacity: 0.8,\r\n        arcLength: 0.5,\r\n        direction: 1,\r\n        speed: 9,\r\n      ),\r\n      RingConfig(\r\n        id: \"e\",\r\n        radius: 100,\r\n        color: Colors.cyan,\r\n        strokeWidth: 8,\r\n        opacity: 0.8,\r\n        arcLength: 0.4,\r\n        direction: -1,\r\n        speed: 8,\r\n      ),\r\n    ];\r\n\r\n    return Scaffold(\r\n      backgroundColor: Colors.white,\r\n      body: Center(child: Loading(rings: rings, size: 400)),\r\n    );\r\n  }\r\n}\r\n<\/code><\/pre>\n<hr \/>\n<p><iframe loading=\"lazy\" title=\"Custom Loading Animation in Flutter &amp; Jetpack Compose \ud83d\ude80 #Shorts\" width=\"540\" height=\"960\" src=\"https:\/\/www.youtube.com\/embed\/L4krcxHhBGM?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/p>\n<hr \/>\n<p><!-- Download Button --><br \/>\n<a href=\"https:\/\/github.com\/alsaeeddev\/flutter-kotlin-loading-animation\/archive\/refs\/heads\/main.zip\">Download Source Code<\/a><\/p>\n<h5>Final Thoughts<\/h5>\n<p>You can make a loading animation just for mobile apps (Jetpack Compose and Flutter), and it can be very rewarding. Both frameworks give developers everything they need to make motion graphics that are expressive without having to use animated assets or libraries from other companies.<\/p>\n<p>This multi-ring spinner is a great example of:<\/p>\n<ul>\n<li>Design of animations that can be changed<\/li>\n<li>Framework-level APIs for drawing<\/li>\n<li>Animation loops that go on forever and work well<\/li>\n<li>Consistency between platforms<br \/>\nA well-designed loader can make your user interface a lot better, whether you&#8217;re making apps just for Android or for more than one platform.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Loading indicators are an important part of making a mobile experience look and feel smooth and professional. A lot of developers use built-in progress bars or spinners, but custom animations can make your app look different, help with branding, and make it feel like it&#8217;s flowing. This guide will show you how to use Jetpack [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":4107,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,223,116,164],"tags":[241,240,245,242,244,243,96],"class_list":["post-4105","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-android","category-flutter","category-kotlin","category-source-codes","tag-custom-loading-animation","tag-custom-loading-animation-for-mobile-app","tag-custom-loading-animation-in-dart","tag-custom-loading-animation-in-flutter","tag-custom-loading-animation-in-jetpack-compose","tag-custom-loading-animation-in-kotlin","tag-jetpack-compose"],"_links":{"self":[{"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/posts\/4105","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/comments?post=4105"}],"version-history":[{"count":10,"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/posts\/4105\/revisions"}],"predecessor-version":[{"id":4111,"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/posts\/4105\/revisions\/4111"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/media\/4107"}],"wp:attachment":[{"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/media?parent=4105"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/categories?post=4105"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/alsaeeddev.com\/shop\/wp-json\/wp\/v2\/tags?post=4105"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}