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
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>
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
}
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>
launch_background.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
task_card_background.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp"/>
<solid android:color="#454545"/>
<stroke
android:width="1dp"
android:color="#555555"/>
</shape>
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" />
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();
},
),
],
);
},
);
},
),
);
}
}
4. Run Project
if everything was done correctly then applicatoin will work.you can add and remove tasks and use widget on homescreen.