In this blog we will see how we can create homescreen widget in our flutter app.If you have tried creating widget then you may know the home_widget package in flutter however we won't be using it to create our widget.i have tried implementing home_widget but i got tons of error and i have even no idea for some error.everytime i solved one error, new error would popup and it was frustrating so i decided to do it without home_widget package.
in this project we will use only one flutter package "shared_preferences".
as we aren't using package for creating widget so we have to write a lot of code.let's get started with sample applicatoin "to do app".
1. Create flutter Project
add "shared_preferences" packages on pubspec.yml file
2.Project Structure
below is our project structure containing all files and folders and we have to create 2 new folder
go to path
widgetnoteapp\android\app\src\main\res
create layout and xml foldercreate all files from below project tree
3. Complete Code
while this project was created the version of flutter was 3.24.3. let's jump to coding part
Widget Integration :
The <receiver>
tag registers the TaskWidgetProvider
as the widget's controller. It links to a configuration file (task_widget_info.xml
) that defines widget properties like size and layout.
Widget Update Service:
A
<service>
tag registers the TaskWidgetService
, which populates the widget’s list dynamically. This service manages how tasks are displayed in the widget.This file ensures the app and widget components are correctly configured, enabling seamless interaction between your app and the home screen widget
AndroidManifest.xml
<application
<!-- add reciever and services for widget -->
<receiver
android:name=".TaskWidgetProvider"
android:label="Task Widget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="com.example.widgetnoteapp.UPDATE_WIDGET" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/task_widget_info" />
</receiver>
<service android:name=".TaskWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<!-- end of reciever and services for widget -->
</application>
Kotlin Directory(Android)
MainActivity.kt:
This file is responsible for launching the main screen of the app. It is the entry point for the app when the user taps on the app icon. The activity sets up the main Flutter view and handles the communication between the Flutter app and native Android components.
TaskWidgetProvider.kt:
This file defines the widget provider, which is responsible for managing widget updates. It fetches data from the app, updates the widget layout, and ensures that the widget reflects the latest task information stored in SharedPreferences
.
TaskWidgetService.kt:
This file manages the widget's content and layout. It provides a service to update the widget with new task data by communicating with the TaskRemoteViewsFactory and refreshing the widget with the latest content.
TaskRemoteViewsFactory.kt:
This file is used to populate the widget with data. It connects with SharedPreferences to retrieve the task list and binds the data to the widget’s views, ensuring that the widget displays the tasks dynamically.
widgetnoteapp\android\app\src\main\kotlin\com\example\widgetnoteapp
TaskWidgetProvider.kt
package com.example.widgetnoteapp
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.util.Log
import android.widget.RemoteViews
class TaskWidgetProvider : AppWidgetProvider() {
private val TAG = "TaskWidgetProvider"
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
Log.d(TAG, "onUpdate called with appWidgetIds: ${appWidgetIds.joinToString()}")
for (appWidgetId in appWidgetIds) {
Log.d(TAG, "Updating widget with ID: $appWidgetId")
updateWidget(context, appWidgetManager, appWidgetId)
}
}
private fun updateWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
Log.d(TAG, "updateWidget called for widget ID: $appWidgetId")
val views = RemoteViews(context.packageName, R.layout.widget_layout)
// Set up the intent for the service
val serviceIntent = Intent(context, TaskWidgetService::class.java)
views.setRemoteAdapter(R.id.task_list_view, serviceIntent)
// Set empty view
views.setEmptyView(R.id.task_list_view, R.id.empty_view)
// Set up the pending intent template for item clicks
val clickIntent = Intent(context, MainActivity::class.java)
val clickPendingIntent = PendingIntent.getActivity(
context,
0,
clickIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
views.setPendingIntentTemplate(R.id.task_list_view, clickPendingIntent)
// Update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
Log.d(TAG, "Widget updated successfully for ID: $appWidgetId")
}
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(TAG, "onReceive called with action: ${intent?.action}")
super.onReceive(context, intent)
if (intent?.action == "com.example.widgetnoteapp.UPDATE_WIDGET") {
Log.d(TAG, "Received custom action to update widget")
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = android.content.ComponentName(context!!, TaskWidgetProvider::class.java)
val appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
Log.d(TAG, "Found widget IDs to update: ${appWidgetIds.joinToString()}")
for (appWidgetId in appWidgetIds) {
updateWidget(context, appWidgetManager, appWidgetId)
}
// Notify that the data changed
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.task_list_view)
} else {
Log.d(TAG, "No custom action matched for intent")
}
}
}
TaskWidgetService.kt
package com.example.widgetnoteapp
import android.content.Intent
import android.widget.RemoteViewsService
class TaskWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return TaskRemoteViewsFactory(applicationContext)
}
}
MainActivity.kt
package com.example.widgetnoteapp
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.appwidget.AppWidgetManager
import android.content.ComponentName
class MainActivity: FlutterActivity() {
private val CHANNEL = "com.example.widgetnoteapp/update_widget"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "updateWidget") {
//log
// Update your widget here
updateWidget()
result.success(null)
} else {
result.notImplemented()
}
}
}
private fun updateWidget() {
// Update widget data
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
val ids = appWidgetManager.getAppWidgetIds(
ComponentName(applicationContext, TaskWidgetProvider::class.java)
)
// This is crucial - it triggers onDataSetChanged() in RemoteViewsFactory
appWidgetManager.notifyAppWidgetViewDataChanged(ids, R.id.task_list_view)
// Send broadcast to update widget
val intent = Intent(applicationContext, TaskWidgetProvider::class.java)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
sendBroadcast(intent)
}
}
TaskRemoteViewsFactory.kt
package com.example.widgetnoteapp
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import android.widget.RemoteViews
import android.widget.RemoteViewsService
class TaskRemoteViewsFactory(private val context: Context) : RemoteViewsService.RemoteViewsFactory {
private var tasks: List<String> = listOf()
private val TAG = "TaskRemoteViewsFactory"
override fun onCreate() {
Log.d(TAG, "onCreate")
loadTasks()
}
override fun onDataSetChanged() {
Log.d(TAG, "onDataSetChanged called")
loadTasks()
Log.d(TAG, "Loaded ${tasks.size} tasks")
}
private fun loadTasks() {
val prefs: SharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val tasksString = prefs.getString("flutter.tasks", "")
Log.d(TAG, "Loading tasks from prefs: $tasksString")
tasks = tasksString?.split(",")?.filter { it.isNotEmpty() } ?: emptyList()
}
override fun onDestroy() {
// No cleanup needed
}
override fun getCount(): Int = tasks.size
override fun getViewAt(position: Int): RemoteViews {
return RemoteViews(context.packageName, R.layout.task_item).apply {
setTextViewText(R.id.task_text, tasks[position])
}
}
override fun getLoadingView(): RemoteViews? = null
override fun getViewTypeCount(): Int = 1
override fun getItemId(position: Int): Long = position.toLong()
override fun hasStableIds(): Boolean = true
}
res/drawable Directory (Android)
glassy_task_background.xml:
This drawable defines a gradient background for the task item in the widget. It gives the task a semi-transparent look, with a smooth gradient from white to transparent, which provides a glassy effect
.
glassy_widget_background.xml:
This drawable provides the background for the entire widget. It features a gradient that creates a dark, translucent effect, making the widget stand out on the home screen. The background is designed to be visually striking yet subtle.
widgetnoteapp\android\app\src\main\res\drawable\
card_background.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="#000000" />
<padding
android:left="8dp"
android:top="8dp"
android:right="8dp"
android:bottom="8dp" />
</shape>
glassy_task_background.xml
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item> <shape android:shape="rectangle"> <corners android:radius="12dp"/> <gradient android:startColor="#66FFFFFF" android:endColor="#40FFFFFF" android:angle="45"/> </shape> </item> <item android:top="1dp" android:left="1dp" android:right="1dp" android:bottom="1dp"> <shape android:shape="rectangle"> <corners android:radius="12dp"/> <gradient android:startColor="#26FFFFFF" android:endColor="#0DFFFFFF" android:angle="45"/> <stroke android:width="0.5dp" android:color="#33FFFFFF"/> </shape> </item></layer-list>
glassy_task_background.xml
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners android:radius="12dp"/>
<gradient
android:startColor="#66FFFFFF"
android:endColor="#40FFFFFF"
android:angle="45"/>
</shape>
</item>
<item android:top="1dp" android:left="1dp" android:right="1dp" android:bottom="1dp">
<shape android:shape="rectangle">
<corners android:radius="12dp"/>
<gradient
android:startColor="#26FFFFFF"
android:endColor="#0DFFFFFF"
android:angle="45"/>
<stroke
android:width="0.5dp"
android:color="#33FFFFFF"/>
</shape>
</item>
</layer-list>
glassy_widget_background.xmlndd.xnml
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners android:radius="16dp"/>
<gradient
android:startColor="#80000000"
android:endColor="#66000000"
android:angle="45"/>
</shape>
</item>
<item android:top="1dp" android:left="1dp" android:right="1dp" android:bottom="1dp">
<shape android:shape="rectangle">
<corners android:radius="16dp"/>
<gradient
android:startColor="#33FFFFFF"
android:endColor="#1AFFFFFF"
android:angle="45"/>
</shape>
</item>
</layer-list>
res/layout Directory (Android)
widget_layout.xml:
This layout file defines the structure of the widget. It includes a TextView to display the widget’s title and a ListView that shows the list of tasks. If no tasks are available, the empty_view text is displayed to inform the user. The layout is designed to be visually appealing with a glassy background.
card_layout.xml:
This layout file defines the appearance of individual task cards. It uses a LinearLayout with padding and rounded corners to create a clean, card-like appearance. Each task is displayed within a card, which allows for a visually appealing task list.
task_layout.xml:
This layout file describes the individual task item layout. It is used within the widget to show each task in a glassy background with rounded corners. It ensures that the tasks are displayed with a modern, clean appearance.
widgetnoteapp\android\app\src\main\res\layout\
card_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/card_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="@drawable/card_background"
android:layout_marginBottom="8dp"
android:elevation="2dp">
<TextView
android:id="@+id/task_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@android:color/black" />
</LinearLayout>
task_item.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/glassy_task_background"
android:padding="12dp"
android:layout_marginHorizontal="4dp">
<TextView
android:id="@+id/task_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:alpha="0.95"/>
</LinearLayout>
widget_layout.xml
RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="@drawable/glassy_widget_background">
<TextView
android:id="@+id/widget_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tasks"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#FFFFFF"
android:alpha="0.95"
android:layout_marginBottom="8dp"
android:paddingStart="4dp"/>
<ListView
android:id="@+id/task_list_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/widget_title"
android:divider="@null"
android:dividerHeight="8dp"/>
<TextView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/widget_title"
android:gravity="center"
android:text="No tasks available"
android:textColor="#E6FFFFFF"
android:padding="16dp"
android:visibility="gone"/>
</RelativeLayout>
widgetnoteapp\android\app\src\main\res\xml\
task_widget_info.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="250dp"
android:minHeight="100dp"
android:updatePeriodMillis="0"
android:initialLayout="@layout/widget_layout"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen" />
This file below contains the main Flutter application logic. It is the core of the app, managing the user interface and interacting with SharedPreferences to save and load tasks. It includes:
TaskInputScreen: A StatefulWidget that allows users to input new tasks. It displays the list of tasks, allows them to add new tasks, and removes tasks when swiped.
SharedPreferences: This is used to store and retrieve tasks persistently. Tasks are stored as a comma-separated string and displayed in the app and widget.
MethodChannel: This is used to communicate between Flutter and the native Android code, allowing the app to update the widget when tasks are added or removed.
The TaskInputScreen is the main screen where users can add tasks and view the list of tasks. It also includes a floating action button to add tasks and a dismissible list to remove tasks.
The app updates the widget in real-time by invoking a method on the native Android side using the platform.invokeMethod('updateWidget').
widgetnoteapp\lib\
main.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/services.dart';
const platform = MethodChannel('com.example.widgetnoteapp/update_widget');
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To-Do Widget App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: TaskInputScreen(),
);
}
}
class TaskInputScreen extends StatefulWidget {
@override
_TaskInputScreenState createState() => _TaskInputScreenState();
}
class _TaskInputScreenState extends State<TaskInputScreen> {
final TextEditingController _taskController = TextEditingController();
String _tasks = ''; // Use a single string to store tasks
//method to update the widget
Future<void> _updateAndroidWidget() async {
try {
// First save data using SharedPreferences
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('tasks', _tasks);
// Then notify the platform to update the widget
await platform.invokeMethod('updateWidget');
} catch (e) {
print('Error updating widget: $e');
}
}
void _saveTask() async {
String taskTitle = _taskController.text;
if (taskTitle.isNotEmpty) {
// Append the new task to the existing tasks
setState(() {
_tasks = _tasks.isEmpty ? taskTitle : '$_tasks,$taskTitle';
});
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('tasks', _tasks); // Make sure the key matches
// Update the widget
await _updateAndroidWidget();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Task added!')),
);
_taskController.clear();
}
}
void _loadSavedTasks() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String tasksString = prefs.getString('tasks') ?? '';
if (tasksString.isNotEmpty) {
setState(() {
_tasks = tasksString;
});
// Update widget when tasks are loaded
await _updateAndroidWidget();
}
}
void _removeTask(int index) async {
List<String> taskList = _tasks.split(',');
if (index < taskList.length) {
taskList.removeAt(index);
setState(() {
_tasks = taskList.join(',');
});
// Update the widget
await _updateAndroidWidget();
}
}
@override
void initState() {
super.initState();
_loadSavedTasks();
}
@override
Widget build(BuildContext context) {
List<String> displayedTasks = _tasks.isNotEmpty
? _tasks.split(',')
: []; // Create a list from the tasks string
return Scaffold(
appBar: AppBar(
title: const Text('To-Do List'),
),
body: ListView.builder(
itemCount: displayedTasks.length,
itemBuilder: (context, index) {
return Dismissible(
key: Key(displayedTasks[index]),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (direction) {
_removeTask(index);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Task removed!'),
));
},
child: Card(
margin: const EdgeInsets.all(10),
elevation: 5,
child: ListTile(
title: Text(
displayedTasks[index],
style: const TextStyle(fontSize: 18),
),
),
),
);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Add Task'),
content: TextField(
controller: _taskController,
decoration: const InputDecoration(hintText: 'Enter task'),
),
actions: <Widget>[
TextButton(
child: const Text('Add'),
onPressed: () {
_saveTask();
Navigator.of(context).pop();
},
),
],
);
},
);
},
),
);
}
}
How Everything Connects
Task Management:
The user interacts with the app to add, view, and delete tasks. When a task is added, it is saved in SharedPreferences, and the widget is updated using a MethodChannel to trigger a refresh on the home screen widget.
Widget Updates:
The widget is powered by Android’s AppWidgetProvider class, which updates its content by fetching the task data from SharedPreferences. The widget layout (widget_layout.xml) is updated by the TaskWidgetService and TaskRemoteViewsFactory, which dynamically bind the task data to the widget views.
SharedPreferences:
All tasks are stored in SharedPreferences, which ensures that data persists across app restarts. The app reads and writes task data to SharedPreferences, ensuring that both the Flutter app and the widget remain in sync.
User Interface:
The Flutter app provides a simple and clean UI for managing tasks. Tasks are displayed in cards (card_layout.xml), and the widget provides a similar layout, ensuring consistency between the app and the widget.
4. Run Project
if everything was done correctly then applicatoin will work.you can add and remove tasks and use widget on homescreen.