Flutter小组件周期刷新

43 阅读4分钟

iOS

效果展示

image.png

代码

//
//  HomeWidgetExample.swift
//  HomeWidgetExample
//
//  Created by Anton Borries on 04.10.20.
//

import SwiftUI
import WidgetKit

private let widgetGroupId = "你自己的GroupId"

// 时间刷新
struct Provider: TimelineProvider {
  func placeholder(in context: Context) -> ExampleEntry {
    ExampleEntry(date: Date(), title: "Placeholder Title", message: "Placeholder Message")
  }

  func getSnapshot(in context: Context, completion: @escaping (ExampleEntry) -> Void) {
      
    fetchWeatherData { weatherInfo in
      let entry = ExampleEntry(
        date: Date(), 
        title: weatherInfo["title"]!,
        message: weatherInfo["detail"]!)
      completion(entry)
    }
  }

  func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
    fetchWeatherData { weatherInfo in
      let currentDate = Date()
      let entry = ExampleEntry(
        date: currentDate, 
        title: weatherInfo["title"]!,
        message:weatherInfo["detail"]!)
      
      // 设置下次更新时间(5分钟后,用于调试)
      let nextUpdate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
      let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
      completion(timeline)
    }
  }
  
  // 获取天气数据
    private func fetchWeatherData(completion: @escaping ([String:String]) -> Void) {
      // 先使用缓存数据
      let groupDefaults = UserDefaults(suiteName: widgetGroupId)
      let title = groupDefaults?.string(forKey: "title") ?? "🚥 "
        let detail = groupDefaults?.string(forKey: "message") ?? ""
      
    let detailString = getCurrentTimeString();
    let urlString = "https://restapi.amap.com/v3/weather/weatherInfo?key=你自己的高德地图的密钥&city=成都"
    
    guard let url = URL(string: urlString) else {
        completion(["title":title,"detail":detailString])
      return
    }
    
      // 网络请求数据自请求
    URLSession.shared.dataTask(with: url) { data, response, error in
      if let data = data {
        do {
          if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
             let lives = json["lives"] as? [[String: Any]],
             let weather = lives.first {
            
            let province = weather["province"] as? String ?? ""
            let city = weather["city"] as? String ?? ""
            let weatherInfo = weather["weather"] as? String ?? ""
            let temperature = weather["temperature"] as? String ?? ""
            
            let weatherString = "🚥 \(province)\(city)\(weatherInfo)\(temperature) "
              // 6. 成功后存入 UserDefaults(更新缓存)
              groupDefaults?.set(weatherString, forKey: "title")
              groupDefaults?.set(detailString, forKey: "message")
              groupDefaults?.synchronize() // 确保立即写入(可选,iOS 12+ 可省略,但加上更稳妥)
              completion(["title":weatherString,"detail":detailString])
          } else {
            completion(["title":title,"detail":detailString])
          }
        } catch {
          completion(["title":title,"detail":detailString])
        }
      } else {
        completion(["title":title,"detail":detailString])
      }
    }.resume()
  }
  
  // 获取当前时间字符串
  private func getCurrentTimeString() -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "HH:mm"
    let timeString = formatter.string(from: Date())
    return "🌞 \(timeString)"
  }
}

// 数据模型
struct ExampleEntry: TimelineEntry {
  let date: Date
  let title: String
  let message: String
}

// UI控件
struct HomeWidgetExampleEntryView: View {
  var entry: Provider.Entry
  let data = UserDefaults.init(suiteName: widgetGroupId)
  let iconPath: String?

  init(entry: Provider.Entry) {
    self.entry = entry
    iconPath = data?.string(forKey: "dashIcon")

  }

  var body: some View {
    VStack(alignment: .leading, spacing: 16) {
      // 顶部信息区域
      HStack(spacing: 12) {
        // 蓝色小车图标
        Image(systemName: "car.fill")
          .font(.system(size: 24))
          .foregroundColor(.blue)
          .frame(width: 32, height: 32)
          .background(
            Circle()
              .fill(Color.blue.opacity(0.1))
          )
        
        VStack(alignment: .leading, spacing: 4) {
            // 交通信息
            HStack(spacing: 6) {
              Text(entry.title)
                .font(.system(size: 12))
                .foregroundColor(.primary)
            }
            
            // 天气信息
            HStack(spacing: 6) {
              Text(entry.message)
                .font(.system(size: 12))
                .foregroundColor(.primary)
            }
        }
      }
        
      // 底部问题区域
      HStack {
        Text("路边划线停车位费用可以不支付吗?")
          .font(.system(size: 14))
          .foregroundColor(.primary)
          .lineLimit(2)
          .widgetURL(URL(string: "homeWidgetExample://message?message=\(entry.message)&homeWidget"))
        
        Spacer()
        
        Image(systemName: "arrow.up.right")
          .font(.system(size: 12))
          .foregroundColor(.blue)
      }
      .padding(.horizontal, 12)
      .padding(.vertical, 16)
      .background(
        RoundedRectangle(cornerRadius: 8)
          .fill(Color.gray.opacity(0.1))
      )
    }
    .padding(8)
    .containerBackground(for: .widget) {
      Color(UIColor.systemBackground)
    }
  }
}

// 中等尺寸小组件配置
struct HomeWidgetEx: Widget {
  let kind: String = "HomeWidgetEx"

  var body: some WidgetConfiguration {
    StaticConfiguration(kind: kind, provider: Provider()) { entry in
      HomeWidgetExampleEntryView(entry: entry)
    }
    .configurationDisplayName("管家")
    .description("智能管家小组件,提供交通和天气信息")
    .supportedFamilies([.systemMedium])
  }
}

// 中等尺寸小组件预览
struct HomeWidgetEx_Previews: PreviewProvider {
    static var previews: some View {
        HomeWidgetExampleEntryView(
            entry: ExampleEntry(date: Date(), title: "🚥 成都 : 限行尾号3、8", message: "🌞 天气 : 晴转多云18℃, 适合洗车")
        )
        .previewContext(WidgetPreviewContext(family: .systemMedium))
        .previewDisplayName("管家小组件")
    }
}

Android

UI代码

package 包名

import HomeWidgetGlanceState
import HomeWidgetGlanceStateDefinition
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.action.ActionParameters
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.cornerRadius
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.color.ColorProvider
import androidx.glance.currentState
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.layout.size
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.text.TextAlign
import es.antonborri.home_widget.HomeWidgetBackgroundIntent
import es.antonborri.home_widget.actionStartActivity
import com.siteya.car_butler.MainActivity
import com.siteya.car_butler.R

class HomeWidgetGlanceAppWidget : GlanceAppWidget() {

  /** Needed for Updating */
  override val stateDefinition = HomeWidgetGlanceStateDefinition()

  override suspend fun provideGlance(context: Context, id: GlanceId) {

    provideContent { GlanceContent(context, currentState()) }
  }

  @Composable
  private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
    val data = currentState.preferences

    val title = data.getString("title", "")!!
    val message = data.getString("message", "")!!
//    val question = data.getString("question", message) ?: message // 问题文本,如果没有则使用message
    val imagePath = data.getString("dashIcon", null)

    Box(
        modifier =
            GlanceModifier.background(ColorProvider(day = Color.White, night = Color.White))
                .padding(horizontal = 16.dp, vertical = 12.dp)
                .clickable(onClick = actionStartActivity<MainActivity>(context))
    ) {
      Column(
          modifier = GlanceModifier.fillMaxSize(),
          verticalAlignment = Alignment.Vertical.Top,
          horizontalAlignment = Alignment.Horizontal.Start,
      ) {
        // 顶部信息区:车图标 + 两行信息
        Row(
            modifier = GlanceModifier.fillMaxWidth(),
            horizontalAlignment = Alignment.Horizontal.Start,
            verticalAlignment = Alignment.Vertical.CenterVertically,

        ) {
          // 左侧车图标
//            Image(
//                provider = ImageProvider(R.drawable.),
//                contentDescription = "应用Logo", // 补充内容描述,提升可访问性
//                modifier = GlanceModifier.size(36.dp)
//            )
            Image(
                provider = ImageProvider(R.drawable.widget_logo),
                contentDescription = null,
                modifier = GlanceModifier.size(36.dp)
            )

//          imagePath?.let {
//
//            val bitmap = BitmapFactory.decodeFile(it)
//            Image(
//                provider = ImageProvider(bitmap),
//                contentDescription = null,
//                modifier = GlanceModifier.size(36.dp)
//            )
//          } ?: run {
//            // 如果没有图片,显示占位
//            Spacer(modifier = GlanceModifier.size(36.dp))
//          }

          Spacer(modifier = GlanceModifier.size(10.dp))
          // 右侧信息
          Column(
              modifier = GlanceModifier.fillMaxWidth(),
              verticalAlignment = Alignment.Vertical.Top
          ) {
            // 第一行:限行信息(title字段)
            Text(
                text = "\uD83D\uDEA5 $title",
                style = TextStyle(
                    fontSize = 23.sp,
                    fontWeight = FontWeight.Normal,
                    color = ColorProvider(day = Color(0xFF333333), night = Color(0xFF333333))
                )
            )

            Spacer(modifier = GlanceModifier.height(4.dp))

            // 第二行:天气信息(message字段)
            Text(
                text = "\uD83C\uDF1E $message",
                style = TextStyle(
                    fontSize = 23.sp,
                    fontWeight = FontWeight.Normal,
                    color = ColorProvider(day = Color(0xFF333333), night = Color(0xFF333333))
                )
            )
          }
        }

        Spacer(modifier = GlanceModifier.height(10.dp))

        // 底部问题框 - 使用圆角
        Box(
            modifier = GlanceModifier
                .fillMaxWidth()
                .cornerRadius(8.dp)
                .background(
                    ColorProvider(day = Color(0xFFF5F5F5), night = Color(0xFFF5F5F5)))
                .padding(horizontal = 10.dp, vertical = 8.dp)
                .clickable(
                    onClick = actionStartActivity<MainActivity>(
                        context,
                        Uri.parse("homeWidgetExample://message?message=$message"),
                    )
                )
        ) {
          Row(
              modifier = GlanceModifier.fillMaxWidth(),
              horizontalAlignment = Alignment.Horizontal.Start,
              verticalAlignment = Alignment.Vertical.Top
          ) {
            Text(
                text = "路边画线停车费用可以不支付吗?",
                style = TextStyle(
                    fontSize = 12.sp,
                    fontWeight = FontWeight.Normal,
                    color = ColorProvider(day = Color(0xFF666666), night = Color(0xFF666666))
                )
            )

            Spacer(modifier = GlanceModifier.size(6.dp))

            // 右侧箭头图标
            Text(
                text = "↗",
                style = TextStyle(
                    fontSize = 14.sp,
                    color = ColorProvider(day = Color(0xFF6FB7FF), night = Color(0xFF6FB7FF))
                )
            )
          }
        }
      }
    }
  }
}

class InteractiveAction : ActionCallback {
  override suspend fun onAction(
      context: Context,
      glanceId: GlanceId,
      parameters: ActionParameters,
  ) {
    val backgroundIntent =
        HomeWidgetBackgroundIntent.getBroadcast(
            context,
            Uri.parse("homeWidgetExample://titleClicked"),
        )
    backgroundIntent.send()
  }
}

Flutter main中的刷新方法

/// Used for Background Updates using Workmanager Plugin
@pragma("vm:entry-point")
void callbackDispatcher() async {
}

安卓刷新方法使用workmanager,在flutter中使用的是home_widget配合使用