Skip to content

Commit

Permalink
feat: migrates data to sqflite instead of shared preferences (#10)
Browse files Browse the repository at this point in the history
* feat: migrates data to sqflite instead of shared preferences

* feat: adds correct theming and version from package

* feat: migrates android widget to use room sqlite databas

* fix: counter reset and editing

* fix: fixes android home screen widget

* fix: accesibility colors for android widget

* feat: adds theme chooser features

todo: improve implementation

* fix: improves theme switch button and code

* fix: removed imports

* build: version bump
  • Loading branch information
CodingAleCR authored Aug 28, 2021
1 parent 07734c8 commit 64d031d
Show file tree
Hide file tree
Showing 69 changed files with 1,675 additions and 277 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ An app based in Flutter that allows you to count days since an incident.

## Coming soon

- Cloud-based storage.
- Account management.
- Multiple counters.
- Account management.
- File-based storage.

_And much more coming soon._
23 changes: 18 additions & 5 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ if (flutterVersionName == null) {
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'

def keystoreProperties = new Properties()
Expand All @@ -33,7 +34,7 @@ if (keystorePropertiesFile.exists()) {
}

android {
compileSdkVersion 28
compileSdkVersion 30

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
Expand All @@ -46,7 +47,7 @@ android {
defaultConfig {
applicationId "codingale.cr.dwi"
minSdkVersion 16
targetSdkVersion 29
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand All @@ -73,7 +74,19 @@ flutter {

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

// FOR WIDGET USAGE
def room_version = "2.3.0"

implementation("androidx.room:room-runtime:$room_version")
annotationProcessor "androidx.room:room-compiler:$room_version"

// To use Kotlin annotation processing tool (kapt)
kapt("androidx.room:room-compiler:$room_version")

// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
}
85 changes: 67 additions & 18 deletions android/app/src/main/java/codingale/cr/dwi/CounterWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,60 @@ import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import android.util.Log
import android.widget.RemoteViews
import androidx.room.Room
import codingale.cr.dwi.database.CounterEntity
import codingale.cr.dwi.database.DATABASE_NAME
import codingale.cr.dwi.database.DWIDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.logging.Logger

class CounterWidget : AppWidgetProvider() {
companion object {
const val SHARED_PREFERENCES_NAME = "FlutterSharedPreferences"
const val PREFIX = "flutter"
const val TITLE = "title"
const val LAST_INCIDENT = "last_incident"
const val RESET_ACTION = "codingale.cr.dwi.RESET_COUNTER"
const val ISO_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS"
}

override fun onReceive(context: Context?, intent: Intent?) {
val manager = AppWidgetManager.getInstance(context)
context?.let {
if (intent?.action == RESET_ACTION) {
val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
val prefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 0)
val editor = prefs.edit()
val manager = AppWidgetManager.getInstance(context)

val widgetId = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)

// Updates counter in storage
val counter = getCounter(context)

val now = Date()
val formatter = SimpleDateFormat(ISO_FORMAT, Locale.US)

editor.putString("$PREFIX.$LAST_INCIDENT", formatter.format(now))
editor.apply()
if (counter != null) {
counter.createdAt = formatter.format(now)
updateCounter(context, counter)
}

// Updates widget
updateAppWidget(context, manager, widgetId)
}
}
super.onReceive(context, intent)
}

override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
Expand All @@ -55,17 +74,28 @@ class CounterWidget : AppWidgetProvider() {
// Enter relevant functionality for when the last widget is disabled
}

private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
val prefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, 0)
private fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
// Default configuration
val defaultTitle = context.getString(R.string.app_title)
val now = Date()
val formatter = SimpleDateFormat(ISO_FORMAT, Locale.US)

val title = prefs.getString("$PREFIX.$TITLE", defaultTitle)
val incidentIsoString = prefs.getString("$PREFIX.$LAST_INCIDENT", formatter.format(now))
val incidentDate = formatter.parse(incidentIsoString)
// Set defaults
var title = defaultTitle
var incidentIsoString = formatter.format(now)
val counter = getCounter(context)
if (counter != null) {
// Update with counter information or fallback to default.
title = counter.title ?: title
incidentIsoString = counter.createdAt ?: incidentIsoString
}
val incidentDate: Date? = formatter.parse(incidentIsoString)

val diff: Long = now.time - incidentDate.time
val diff: Long = now.time - incidentDate!!.time
val days = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS).toInt()

val daysString = context.resources.getQuantityString(R.plurals.counter_text, days)
Expand All @@ -81,10 +111,29 @@ class CounterWidget : AppWidgetProvider() {
resetIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
resetIntent.data = Uri.parse(resetIntent.toUri(Intent.URI_INTENT_SCHEME))

val pendingIntent = PendingIntent.getBroadcast(context, 0, resetIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val pendingIntent =
PendingIntent.getBroadcast(context, 0, resetIntent, PendingIntent.FLAG_UPDATE_CURRENT)
views.setOnClickPendingIntent(R.id.button, pendingIntent)

// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}

private fun getCounter(context: Context): CounterEntity? = runBlocking {
return@runBlocking withContext(Dispatchers.IO) {
val db = DWIDatabase.getDatabase(context)
val dao = db.counterDao()
val counters = dao.getAll()
Log.d("Counters ->>>>>", "Length ${counters.size}")
return@withContext counters.firstOrNull()
}
}

private fun updateCounter(context: Context, counter: CounterEntity) = runBlocking {
return@runBlocking withContext(Dispatchers.IO) {
val db = DWIDatabase.getDatabase(context)
val dao = db.counterDao()
dao.update(counter)
}
}
}
18 changes: 18 additions & 0 deletions android/app/src/main/java/codingale/cr/dwi/database/CounterDAO.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package codingale.cr.dwi.database

import androidx.room.Dao
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update

@Dao
interface CounterDAO {
@Query("SELECT * FROM time_counters")
suspend fun getAll(): List<CounterEntity>

@Query("SELECT * FROM time_counters WHERE id = :counterId")
suspend fun findById(counterId: String): CounterEntity

@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(counter: CounterEntity)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package codingale.cr.dwi.database

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "time_counters")
data class CounterEntity (
@PrimaryKey @ColumnInfo(name = "id") val id: String,
@ColumnInfo(name = "title") var title: String?,
@ColumnInfo(name = "created_at") var createdAt: String?
)
35 changes: 35 additions & 0 deletions android/app/src/main/java/codingale/cr/dwi/database/DWIDatabase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package codingale.cr.dwi.database

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [CounterEntity::class], version = 1)
abstract class DWIDatabase : RoomDatabase() {
abstract fun counterDao(): CounterDAO

companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile
private var INSTANCE: DWIDatabase? = null

fun getDatabase(context: Context): DWIDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
DWIDatabase::class.java,
DATABASE_NAME
)
.enableMultiInstanceInvalidation()
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package codingale.cr.dwi.database

const val DATABASE_NAME= "dwi_local.db"
1 change: 1 addition & 0 deletions android/app/src/main/res/layout/counter_widget.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:fontFamily="sans-serif-black"
android:text="Reset"
android:textColor="@color/primary" />

Expand Down
2 changes: 1 addition & 1 deletion android/app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#56F0D4</color>
<color name="primary">#4400ff</color>
<color name="ic_launcher_background">#56F0D4</color>
</resources>
4 changes: 2 additions & 2 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.5.21'
repositories {
google()
jcenter()
}

dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath 'com.android.tools.build:gradle:7.0.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand Down
2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
3 changes: 2 additions & 1 deletion data/lib/data.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
library data;

export 'repositories/repositories.dart';
export 'local/database/database.dart';
export 'services/services.dart';
2 changes: 2 additions & 0 deletions data/lib/local/database/constants/constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const kDatabaseName = "dwi_local.db";
const kDatabaseVersion = 1;
54 changes: 54 additions & 0 deletions data/lib/local/database/database.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'dart:io';

export 'constants/constants.dart';
export 'migrations/migrations.dart';
export 'repositories/repositories.dart';
export 'entities/entities.dart';
export 'support/support.dart';

import 'package:data/local/database/constants/constants.dart';
import 'package:data/local/database/migrations/migrations.dart';
import 'package:sqflite/sqflite.dart';

_onConfigure(Database db) async {
// Adds support for cascade delete
await db.execute("PRAGMA foreign_keys = ON");
}

_onCreate(Database db, int version) async {
await migrations[version]?.create(db);
}

_onUpgrade(Database db, int oldVersion, int newVersion) async {
int pendingQty = newVersion - oldVersion;
List<int> pendingMigrations = List.generate(pendingQty, (i) => i + 1);

await Future.forEach<int>(pendingMigrations, (versionDiff) async {
await migrations[newVersion]?.up(db);
});
}

_onDowngrade(Database db, int oldVersion, int newVersion) async {
int pendingQty = newVersion - oldVersion;
List<int> pendingMigrations = List.generate(pendingQty, (i) => i + 1);

await Future.forEach<int>(pendingMigrations, (versionDiff) async {
await migrations[newVersion]?.down(db);
});
}

Future<Database> openDWIDatabase() async {
var databasesPath = await getDatabasesPath();
var path = databasesPath + '/' + kDatabaseName;

await Directory(databasesPath).create(recursive: true);

return await openDatabase(
path,
version: kDatabaseVersion,
onConfigure: _onConfigure,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
onDowngrade: _onDowngrade,
);
}
1 change: 1 addition & 0 deletions data/lib/local/database/entities/entities.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'time_counter.entity.dart';
Loading

0 comments on commit 64d031d

Please sign in to comment.