fbpx

How to Build Wallpaper App With Flutter

How to Build Wallpaper App With Flutter

Flutter was introduced by Google in 2018. It is a cross-platform mobile application development framework based on the dart programming language. It is quite young in comparison to other app development frameworks but in no time has gained popularity which is still growing. The framework is based on a dart programming language which is quite similar to Java as well as JavaScript.

The coding structure is a java based on Widget based UI implementations. Widget-based UI implementation makes the UI development for the app simpler and easy. Not to mention, it offers pixel-by-pixel app interface development tools that help the app look modern, beautiful, and intuitive.

Why Flutter…

Flutter is becoming a staple for mobile app developers and development companies because:

  • It offers a pixel-perfect app interface development tool to implement beautiful and modern app UI.
  • Easy UI building using Widgets.
  • Flutter uses widgets to implement user interface instead of native components that make use of device GPU to render the UI on the screen.
  • Dart codes are directly compiled to run in the CPU of the device, which makes the flutter developed apps quite faster.
  • Easy setup and well-maintained libraries.
  • Code once and build for both Android and iOS platforms.
  • Flutter web as well as desktop app framework available now.

Problem Statement

Though Flutter is emerging as an ideal framework for cross-platform mobile development, it has issues that are yet to be addressed. Most of the issues are open because the tech is quite young and the community is still in its growing phase. One of the major issues that a developer will have to face is the lack of available libraries to get the work done.

The available libraries are comparatively new and may contain issues. Some of the libraries may not even work in the latest version of Android and Flutter SDK. This can be a major problem where developers will have to get their hands dirty on native Android codes.

Solution

One of the most efficient solutions to tackle this lack of availability of packages is to connect the flutter codes with native Kotlin codes. In other words, establish communication between them. A way to establish communication is from the Flutter Method Channels. Using this Method Channel service the dart code from the flutter framework can call the functions defined in native code.

In this tutorial, we are going to learn the same thing. We are going to learn how to make use of Flutter Method Channels to communicate with Native codes in the Kotlin file. Here, we are going to implement a Flutter wallpaper app that has the feature to set up the background wallpaper of the device.

Now, there are lots of wallpaper libraries in the market that can help us with that. But, one problem commonly faced is the issue with the latest version of Android. The packages are not devised to accommodate the changes in the latest Android version X. Hence, this tutorial is going to demonstrate how to set up the background wallpaper of the Android device communication with the native Android code using the Flutter method channel. In the course of learning the solution, we will also implement a simple flutter app with beautiful UI implementation.

So, let’s get started!

Create a new Flutter project

First, we need to create a new Flutter project. For that, make sure that the Flutter SDK and other flutter app development related requirements are properly installed. If everything is properly set up, then in order to create a project, we can simply run the following command in the desired local directory:

flutter create backgroundSolution

After the project has been set up, we can navigate inside the project directory and execute the following command in the terminal to run the project in either an available emulator or an actual device:

flutter run

After successfully build, we will get the following result on the emulator screen:

Organizing the project structure

Now that we have the flutter project, we are going to organize for project directory structure first. Our main.dart file is kept in the ./lib directory. We need to create two extra folders inside the ./lib directory. One for the screens and the other for the custom widgets. The project directory structure inside the ./lib directory is demonstrated in the screenshot below:

Once we have done that, we can move on to creating screens.

Creating Home screen inside screens

First, we are going to work on implementing the Home screen. For that, we need to create a file named Home.dart inside the ./screens directory that we created before. Inside the Home.dart file, we are going to implement a Stateful widget as shown in the code snippet below:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:ui';

class HomePage extends StatefulWidget {
  HomePage({Key key}) : super(key: key);
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<HomePage> {

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            backgroundColor: Colors.white,
            centerTitle: true,
            title: Text(
              "Wallpaper",
              style: TextStyle(color: Colors.black87, fontFamily: 'Overpass', fontSize: 20),
            ),
            elevation: 0.0
        ),
        backgroundColor: Colors.white,
	  body: Container()
    );
  }
}

Here, we have returned a simple Scaffold widget with an app bar. The app bar is simple with a styled Text widget.

To show the Home screen as the first screen after launching the app, we need to import it in main.dart as directed in the code snippet below:

import 'package:backgroundSolution/screens/Home.dart';

Then, we need to remove all the default code in the main.dart file and call Home screen in the home property of MaterialApp widget as shown in the code snippet below:

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Wallpaper',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: HomePage(),
    );
  }
}

Hence, we will get the following result on the emulator screen:

As we can see, we have got an App bar at the top of the Home screen.

Adding Images to Scaffold Body

Now, we are going to add images to the Scaffold widget body section which had an empty Container before.

But first, we need to create a list that stores some mock image URLs. Here, we have taken some portrait images from pexels and stored it in an imagesPath list as shown in the code snippet below:

Widget build(BuildContext context) {
    List imagesPath = [
      "https://images.pexels.com/photos/5778749/pexels-photo-5778749.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
      "https://images.pexels.com/photos/2440061/pexels-photo-2440061.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
      "https://images.pexels.com/photos/2882234/pexels-photo-2882234.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940",
      "https://images.pexels.com/photos/1192332/pexels-photo-1192332.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940"
    ];
    return Scaffold(

Then, we are going to try and add an image to the body Container using the Image.network widget as shown in the code snippet below:

body: SingleChildScrollView(
          child: Container(
            color: Colors.white,
            child: Column(
              children: [
                SizedBox(height: 10),
                Image.network(imagesPath[0]),
                SizedBox(
                  height: 24,
                ),
              ],
            ),
          ),
        ),

Here, we have also added some extra widgets to the body in order to make the Scaffold body appealing.

Hence, we will get the result as shown in the emulator screenshot below:

Showing Images in Grid format

Until now, we only showed a single image of the body. But now, we are going to display the images stored in the imagesPath list as a grid. For that, we need to create a separate custom widget file called widget.dart inside the ./widgets directory.

In widget.dart, we are going to create a custom widget called wallpaper that takes in a list of images and a parent context as parameters. The overall implementation of the custom widget is provided in the code snippet below:

import 'package:flutter/material.dart';

Widget wallPaper(List imagesPath, BuildContext context) {
  return Container(
    padding: EdgeInsets.symmetric(horizontal: 16),
    child: GridView.count(
        crossAxisCount: 2,
        childAspectRatio: 0.6,
        physics: ClampingScrollPhysics(),
        shrinkWrap: true,
        padding: const EdgeInsets.all(4.0),
        mainAxisSpacing: 6.0,
        crossAxisSpacing: 6.0,
        children: imagesPath.map((imagePath) {
          return GridTile(
            child: GestureDetector(
              onTap: () {},
              child: Hero(
                tag: imagePath,
                child: Container(
                  child: ClipRRect(
                      borderRadius: BorderRadius.circular(16),
                      child:  Image.network(
                              imagePath,
                              height: 50,
                              width: 100,
                              fit: BoxFit.cover,
                     )
                  ),
                ),
              )
            )
          );
        }).toList()),
  );
}

Here, we have made use of the GridView widget as a child of the parent Container. The count method of the GridView widget is configured with various properties. Lastly, we have used the Image.network widget to display the image. The GestureDetector widget that we have used has an onTap method property that allows us to trigger something when clicked on the image.

To use this custom widget, we need to import it to the Home.dart file as directed in the code snippet below:

import 'package:backgroundSolution/widgets/widget.dart';

Then, we need to use the wallpaper widget instead of Image widget bypassing the imagesPath list and context as shown in the code snippet below:

body: SingleChildScrollView(
          child: Container(
            color: Colors.white,
            child: Column(
              children: [
                SizedBox(height: 10),
                wallPaper(imagesPath, context),
                SizedBox(
                  height: 24,
                ),
              ],
            ),
          ),
        ),

Now, we can get the images showcased beautifully in the Grid style as demonstrated in the emulator screenshot below:

Image View Screen

Now, we are going to create an Image View screen that will display a single image on an entire screen. We will navigate to this screen once we click on images on Grid. First, we need to create a new file called ImageView.dart inside the ./screens directory. Inside the ImageView.dart, we need to apply the following code:

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';


class ImageView extends StatefulWidget {
  final String imgPath;

  ImageView({@required this.imgPath});

  @override
  _ImageViewState createState() => _ImageViewState();
}

class _ImageViewState extends State<ImageView> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: <Widget>[
          Hero(
            tag: widget.imgPath,
            child: Container(
              height: MediaQuery.of(context).size.height,
              width: MediaQuery.of(context).size.width,
              child: Image.network(widget.imgPath, fit: BoxFit.cover)
            ),
          ),
        ],
      ),
    );
  }
}

Here, we have returned a Scaffold widget with Container holding image. This page also accepts a parameter called imgPath. We have to send this imgPath parameter when navigating to this screen. Now in order to navigate to this screen, we need to add the navigation code in the GestureDetector‘s onTap method in the wallpaper widget of the widget.dart file. The code is provided in the code snippet below:

child: GestureDetector(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ImageView(
                      imgPath: imagePath,
                    )
                  )
                );
              },

We can also notice a Hero widget in both ImageView.dart and widget.dart file. This Hero widget is used to animate when navigating. It will give a zooming animation effect when navigating in and out of the Image View Screen.

The result is shown in the demo below:

Setting Image as Wallpaper

Now comes the most important step. Here, we are going to configure UI as well as functions in order to set the device wallpaper. But first, we need to install the required dependencies as directed in the code snippet below:

material_design_icons_flutter: 4.0.5045
permission_handler: ^5.0.1+1
path_provider: ^1.6.18
dio: ^3.0.10

Here, we are installing four dependencies:

  • material_design_icons_flutter: This package provides a set of beautiful material design icons for Flutter.
  • permission_handler: This package allows us to handle permissions to devise storage, gallery, etc in case we don’t have access to them.
  • path_provider: This package allows us to fetch paths to the storage, application directory, etc.
  • dio: It is an HTTP client package that supports Interceptors, Global configuration, Form Data, Request Cancellation, File downloading, Timeout, etc.

We are going to make use of each one of them here in this step.

First, we need to import these packages to ImageView.dart file:

import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:io';
import 'dart:ui';
import 'package:dio/dio.dart';

Adding Buttons to View

To set the wallpaper, we need something to trigger the action. For that, we need to buttons on the Image View screen. The code for the buttons is provided in the code snippet below:

body: Stack(
        children: <Widget>[
          Hero(
            tag: widget.imgPath,
            child: Container(
              height: MediaQuery.of(context).size.height,
              width: MediaQuery.of(context).size.width,
              child: Image.network(widget.imgPath, fit: BoxFit.cover)
            ),
          ),
          Container(
            height: MediaQuery.of(context).size.height,
            width: MediaQuery.of(context).size.width,
            alignment: Alignment.bottomCenter,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                InkWell(
                    onTap: () {},
                    child: Stack(
                      children: <Widget>[
                        Container(
                          width: MediaQuery.of(context).size.width / 2,
                          height: 50,
                          decoration: BoxDecoration(
                            color: Color(0xff1C1B1B).withOpacity(0.8),
                            borderRadius: BorderRadius.circular(40),
                          ),
                        ),
                        Container(
                            width: MediaQuery.of(context).size.width / 2,
                            height: 50,
                            alignment: Alignment.center,
                            decoration: BoxDecoration(
                                border:
                                    Border.all(color: Colors.white24, width: 1),
                                borderRadius: BorderRadius.circular(40),
                                gradient: LinearGradient(
                                    colors: [
                                      Color(0x36FFFFFF),
                                      Color(0x0FFFFFFF)
                                    ],
                                    begin: FractionalOffset.topLeft,
                                    end: FractionalOffset.bottomRight)),
                            child: Column(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: <Widget>[
                                Text(
                                  "Set Wallpaper",
                                  style: TextStyle(
                                      color: Colors.white70,
                                      fontSize: 15,
                                      fontWeight: FontWeight.w500),
                                ),
                                SizedBox(
                                  height: 1,
                                ),
                              ],
                            )),
                      ],
                    )),
                SizedBox(
                  height: 16,
                ),
                InkWell(
                  onTap: () {
                    Navigator.pop(context);
                  },
                  child: Text(
                    "Cancel",
                    style: TextStyle(
                        color: Colors.white60,
                        fontSize: 12,
                        fontWeight: FontWeight.w500),
                  ),
                ),
                SizedBox(
                  height: 50,
                )
              ],
            ),
          )
        ],
      ),

Hence, we will get the result as shown in the emulator screenshot below:

Showing Modal Dialog For Setting Wallpaper Options

Here, we are not going to set the Home screen wallpaper of the device but also device a feature to set the Lock Screen Wallpaper. For that, we need to provide options. We are going to show those options in a Modal dialog once the user clicks on the ‘Set Wallpaper’ button. For that, we are going to return the showDialog widget with the required UI configurations as directed in the code snippet below:

InkWell(
  onTap: () {
    return showDialog(
    context: context,
    builder: (BuildContext context) {
      return Dialog(
        shape: RoundedRectangleBorder(
            borderRadius:
                BorderRadius.circular(20.0)), //this right here
        child: Container(
          height: 200,
          child: Padding(
            padding: const EdgeInsets.all(12.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.start,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                ButtonTheme(
                  height: 50,
                  minWidth: double.infinity,
                  child: FlatButton(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    child: Row(
                      children: [
                        Icon(
                          MdiIcons.home
                        ),
                        SizedBox(width : 10),
                        Text(
                          "Home Screen", style: TextStyle(color: Colors.black87, fontSize: 16, wordSpacing: 2),
                        ),
                      ],
                    ),
                  ),
                ),
                SizedBox(height: 10,),
                ButtonTheme(
                  height: 50,
                  minWidth: double.infinity,
                  child: FlatButton(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    child: Row(
                      children: [
                        Icon(
                          MdiIcons.lock
                        ),
                        SizedBox(width : 10),
                        Text(
                          "Lock Screen", style: TextStyle(color: Colors.black87, fontSize: 16, wordSpacing: 2),
                        ),
                      ],
                    ),
                  ),
                ),
                SizedBox(height: 10,),
                ButtonTheme(
                  height: 50,
                  minWidth: double.infinity,
                  child: FlatButton(
                    onPressed: () {
                      Navigator.pop(context);
                    },
                    child: Row(
                      children: [
                        Icon(
                          MdiIcons.wallpaper
                        ),
                        SizedBox(width : 10),
                        Text(
                          "Both Screens", style: TextStyle(color: Colors.black87, fontSize: 16, wordSpacing: 2),
                        ),
                      ],
                    ),
                  ),
                )
              ],
            ),
          ),
        ),
      );
    });
  },
  child: Stack(

Hence, we will get the Modal Dialog with three options as shown in the demo below:

Here, we have made use of the material icons. Now the aim is to set the wallpaper of the device based on the option selected in the Modal dialog.

We have the UI ready. Now, it is time to formulate the logic as well.

Downloading Image and Setting it as Wallpaper

Here, we are going to download the image using the dio package and then set the wallpaper using the Method channel. The code to download the image to the device storage is provided in the code snippet below:

_save(int screenType) async {
    await Permission.storage.request();
    print("${widget.imgPath}");
    if(await Permission.storage.request().isGranted){
      final dir = await getExternalStorageDirectory();
      Dio dio = new Dio();
      dio.download(
        widget.imgPath,
        "${dir.path}/myimage.jpeg",
        onReceiveProgress: (rcv, total) {
          print(
              'received: ${rcv.toStringAsFixed(0)} out of total: ${total.toStringAsFixed(0)}');
        },
        deleteOnError: true,
      ).then((_) {
      });
    }
  }

This _save function first asks permission to access the device storage. Once granted, the dio package downloads the image into the device storage. The function accepts the screenType parameter as well.

The screenType parameter refers to a integer number 1 to 3, where

  • 1: For Home Screen
  • 2: For Lock Screen
  • 3: For Both Screen

This screenType parameter is to be sent to the native code in order to set the wallpaper.

But first, we need to call the function in the respective options of the Modal dialog passing respective screen types as shown in the code snippet below:

 

For Home Screen:

FlatButton(
 onPressed: () {
   _save(homeScreenType);
   Navigator.pop(context);
},

 

For Lock Screen:

FlatButton(
  onPressed: () {
    _save(lockScreenType);
    Navigator.pop(context);
  },

 

For Both Screens:

FlatButton(
  onPressed: () {
    _save(bothScreenType);
    Navigator.pop(context);
  },

Implement Method Channel

Here, we are going to work on the native code. We are going to implement a Method channel containing code to set up the device wallpaper. Then by using this method channel the flutter code can communicate with the functions inside this native code file in order to set the wallpaper.

First, we need to open the file MainActivity.kt in the path ./android/app/src/main/kotlin/. It is a Kotlin file which will appear empty. Here, we are going to implement the Native Android code channel in order to set the Device wallpaper. The overall code is provided in the code snippet below:

package com.example.backgroundSolution

import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
import java.io.IOException
import android.app.WallpaperManager
import android.graphics.BitmapFactory
import java.io.File
import android.os.Build
import android.annotation.TargetApi
import android.content.Context
import io.flutter.Log

private const val CHANNEL = "com.example.backgroundSolution/wallpaper"
class MainActivity: FlutterActivity() {

   override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
      if (call.method == "setWallpaper") {
        val arguments = call.arguments as ArrayList<*>
        val setWallpaper = setWallpaper(arguments[0] as String, applicationContext, arguments[1] as Int)

        if (setWallpaper == 0) {
          result.success(setWallpaper)
        } else {
          result.error("UNAVAILABLE", "", null)
        }
      } else {
            result.notImplemented()
      }
    }
  }

  private fun setWallpaper(path: String, applicationContext: Context, wallpaperType: Int): Int {
    var setWallpaper = 1
    val bitmap = BitmapFactory.decodeFile(path)
    val wm: WallpaperManager? = WallpaperManager.getInstance(applicationContext)
    setWallpaper = try {
      wm?.setBitmap(bitmap, null, true, wallpaperType)
      0
    } catch (e: IOException) {
      1
    }

    return setWallpaper
  }
}

Here, we have imported all the required dependencies required in the latest Android version. Then, we have defined a channel as "com.example.backgroundSolution/wallpaper". Then, we have a java class called MainActivity which houses two functions. One is configureFlutterEngine which is the main function that sets the wallpaper. Another is setWallpaper function that is going to be called from the flutter side. This function receives the image path and wallpaper type from the flutter code and then returns the value to the main function.

Now in order to create the Flutter method channel, we need to import the services.dart file inside the ImageView.dart file as directed in the code snippet below:

import 'package:flutter/services.dart';

Also, we need to define the screen type variables which were mentioned before as shown in the code snippet below:

int homeScreenType = 1;
int lockScreenType = 2;
int bothScreenType = 3;

Inside the class object of ImageView, we need to Initialize the method channel as shown in the code snippet below:

static const platform = const MethodChannel('com.example.backgroundSolution/wallpaper');

The channel name inside the MethodChannel instance should match the one in the MainActivity.kt file. Here, the instance of the channel is store in the platform constant.

Devise the function to communicate with method channel native code

Here, we are going to create a function to communicate with the native code in MainActivity.kt file through the method channel. Using the platform constant, we are going to invoke the setWallpaper method defined in MainActivity.kt passing the imagePath and wallpaperType as parameters. The overall implementation of the function is provided in the code snippet below:

Future<void> _setWallpaper(imagePath ,int wallpaperType) async {
  try {
    final int result = await platform
        .invokeMethod('setWallpaper', [imagePath, wallpaperType]);
    print('Wallpaper Updated.... $result');
  } on PlatformException catch (e) {
    print("Failed to Set Wallpaer: '${e.message}'.");
  }
}

Here, we are communicating with the setWallpaper method in the MainActivity.kt file and passing the required parameters to set the wallpaper.

Lastly, we just need to call this function after the wallpaper has been downloaded in the _save function:

_save(int screenType) async {
    await Permission.storage.request();
    print("${widget.imgPath}");
    if(await Permission.storage.request().isGranted){
      final dir = await getExternalStorageDirectory();
      Dio dio = new Dio();
      dio.download(
        widget.imgPath,
        "${dir.path}/myimage.jpeg",
        onReceiveProgress: (rcv, total) {
          print(
              'received: ${rcv.toStringAsFixed(0)} out of total: ${total.toStringAsFixed(0)}');
        },
        deleteOnError: true,
      ).then((_) {
        _setWallpaper("${dir.path}/myimage.jpeg", screenType);
      });
		}
}

Hence, the final demo of setting the device wallpaper is demonstrated below:

As we can notice, the wallpaper of the Device Home Screen changes once we set it. Similarly, we can set the wallpaper of the Lock screen as well as both at the same time. The only value determining this is the integer values that we defined below.

Hence, the result demonstrates that we have successfully devised the Flutter method channel in order to establish functional communication between the Flutter code and Native code.

Conclusion

The main aim of this tutorial was to demonstrate the communication between flutter code and native code using the Flutter method channel. With it, the tutorial also successfully delivered the stepwise implementation of the Flutter Wallpaper app UI as well as functionality.

There was a lot to learn about flutter in this long tutorial. Hope it was interesting and delivered the knowledge it was meant to deliver. Now, the challenge to apply such communication mechanism using flutter method channels to devise other features related to the device such as checking the device battery level, internet connectivity, hotspot enabling, etc.

Please Subscribe To Newsletter For Unlock Full Source Code

Loading...

The overall code is available on GitHub.

Until next time, Happy Coding!


There is no ads to display, Please add some

krissanawat

krissanawat

React Native developer from beautiful Chiangmai, love Americano and travel so much