Renamed old app into Donext

Added DonextV2 module with Compose and Room
Set up clean architecture
Add DI with Hilt
Setup initial database
Display task lists on main activity
This commit is contained in:
Geoffroy Bonneville
2025-09-10 18:40:00 -04:00
parent b2672560b7
commit d6e05c17ba
210 changed files with 832 additions and 4 deletions

View File

@@ -0,0 +1,24 @@
package com.wismna.geoffroy.donext
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.wismna.geoffroy.donext", appContext.packageName)
}
}

View File

@@ -0,0 +1,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".DonextApplication">
<activity
android:name="com.wismna.geoffroy.donext.presentation.MainActivity"
android:exported="true"
android:label="@string/title_activity_main"
android:theme="@style/Theme.DoNext">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,8 @@
package com.wismna.geoffroy.donext
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class DonextApplication: Application() {
}

View File

@@ -0,0 +1,17 @@
package com.wismna.geoffroy.donext.data
import androidx.room.TypeConverter
import java.time.Instant
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Instant? {
return value?.let { Instant.ofEpochMilli(it) }
}
@TypeConverter
fun instantToTimestamp(instant: Instant?): Long? {
return instant?.toEpochMilli()
}
}

View File

@@ -0,0 +1,41 @@
package com.wismna.geoffroy.donext.data
import com.wismna.geoffroy.donext.data.entities.TaskEntity
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.model.TaskList
import java.time.Instant
fun TaskEntity.toDomain() = Task(
id = id,
name = name,
isDone = isDone,
taskListId = taskListId,
description = description,
cycles = cycles,
isDeleted = isDeleted,
updateDate = Instant.ofEpochMilli(updateDate)
)
fun Task.toEntity() = TaskEntity(
id = id,
name = name,
isDone = isDone,
taskListId = taskListId,
description = description,
cycles = cycles,
isDeleted = isDeleted,
updateDate = updateDate.toEpochMilli()
)
fun TaskListEntity.toDomain() = TaskList(
id = id,
name = name,
isDeleted = isDeleted
)
fun TaskList.toEntity() = TaskListEntity(
id = id,
name = name,
isDeleted = isDeleted
)

View File

@@ -0,0 +1,22 @@
package com.wismna.geoffroy.donext.data.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "tasks")
data class TaskEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val description: String,
val cycles: Int = 0,
@ColumnInfo(name = "done")
val isDone: Boolean = false,
@ColumnInfo(name = "deleted")
val isDeleted: Boolean = false,
@ColumnInfo(name = "task_list_id")
val taskListId: Long,
@ColumnInfo(name = "update_date")
val updateDate: Long = System.currentTimeMillis()
)

View File

@@ -0,0 +1,12 @@
package com.wismna.geoffroy.donext.data.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "task_lists")
data class TaskListEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val isDeleted: Boolean = false
)

View File

@@ -0,0 +1,31 @@
package com.wismna.geoffroy.donext.data.injection
import android.content.Context
import com.wismna.geoffroy.donext.data.local.AppDatabase
import com.wismna.geoffroy.donext.data.local.dao.TaskDao
import com.wismna.geoffroy.donext.data.local.dao.TaskListDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(
@ApplicationContext context: Context
): AppDatabase {
return AppDatabase.buildDatabase(context)
}
@Provides
fun provideTaskDao(db: AppDatabase): TaskDao = db.taskDao()
@Provides
fun provideTaskListDao(db: AppDatabase): TaskListDao = db.taskListDao()
}

View File

@@ -0,0 +1,18 @@
package com.wismna.geoffroy.donext.data.injection
import com.wismna.geoffroy.donext.data.local.repository.TaskRepositoryImpl
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindTaskRepository(
impl: TaskRepositoryImpl
): TaskRepository
}

View File

@@ -0,0 +1,65 @@
package com.wismna.geoffroy.donext.data.local
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.wismna.geoffroy.donext.data.Converters
import com.wismna.geoffroy.donext.data.local.dao.TaskDao
import com.wismna.geoffroy.donext.data.local.dao.TaskListDao
import com.wismna.geoffroy.donext.data.entities.TaskEntity
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Database(
entities = [TaskEntity::class, TaskListEntity::class],
version = 7
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
abstract fun taskListDao(): TaskListDao
companion object {
@Volatile
private var DB_INSTANCE: AppDatabase? = null
val MIGRATION_6_7 = object : Migration(6, 7) {
override fun migrate(db: SupportSQLiteDatabase) {
// TODO: migrate from old Donext database (v6)
}
}
fun buildDatabase(context: Context): AppDatabase {
return DB_INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context,
AppDatabase::class.java,
"donext.db"
)
.addMigrations(MIGRATION_6_7)
.fallbackToDestructiveMigration(false)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
// insert default lists
CoroutineScope(Dispatchers.IO).launch {
val dao = DB_INSTANCE?.taskListDao()
dao?.insertTaskList(TaskListEntity(name = "Work"))
dao?.insertTaskList(TaskListEntity(name = "Personal"))
dao?.insertTaskList(TaskListEntity(name = "Shopping"))
}
}
})
.build()
DB_INSTANCE = instance
return instance
}
}
}
}

View File

@@ -0,0 +1,32 @@
package com.wismna.geoffroy.donext.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.wismna.geoffroy.donext.data.entities.TaskEntity
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks WHERE task_list_id = :listId")
suspend fun getTasksForList(listId: Long): List<TaskEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: TaskEntity)
@Update
suspend fun updateTask(task: TaskEntity)
@Query("UPDATE tasks SET done = :done, update_date = :updateDate WHERE id = :taskId")
suspend fun markTaskDone(taskId: Long, done: Boolean, updateDate: Long = System.currentTimeMillis())
@Query("UPDATE tasks SET deleted = :deleted, update_date = :updateDate WHERE id = :taskId")
suspend fun markTaskDeleted(taskId: Long, deleted: Boolean, updateDate: Long = System.currentTimeMillis())
@Query("UPDATE tasks SET cycles = cycles + 1, update_date = :updateDate WHERE id = :taskId")
suspend fun increaseCycle(taskId: Long, updateDate: Long = System.currentTimeMillis())
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskListId")
suspend fun deleteAllTasksFromList(taskListId: Long, deleted: Boolean)
}

View File

@@ -0,0 +1,24 @@
package com.wismna.geoffroy.donext.data.local.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface TaskListDao {
@Query("SELECT * FROM task_lists WHERE isDeleted = 0")
fun getTaskLists(): Flow<List<TaskListEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTaskList(taskList: TaskListEntity)
@Update
suspend fun updateTaskList(taskList: TaskListEntity)
@Query("UPDATE task_lists SET isDeleted = :isDeleted WHERE id = :listId")
suspend fun deleteTaskList(listId: Long, isDeleted: Boolean)
}

View File

@@ -0,0 +1,56 @@
package com.wismna.geoffroy.donext.data.local.repository
import com.wismna.geoffroy.donext.data.local.dao.TaskDao
import com.wismna.geoffroy.donext.data.local.dao.TaskListDao
import com.wismna.geoffroy.donext.data.toDomain
import com.wismna.geoffroy.donext.data.toEntity
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.model.TaskList
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.time.Instant
import javax.inject.Inject
class TaskRepositoryImpl @Inject constructor(
private val taskDao: TaskDao,
private val taskListDao: TaskListDao
): TaskRepository {
override suspend fun getTasksForList(listId: Long): List<Task> {
return taskDao.getTasksForList(listId).map { it.toDomain() }
}
override suspend fun insertTask(task: Task) {
taskDao.insertTask(task.toEntity())
}
override suspend fun updateTask(task: Task) {
val updated = task.copy(updateDate = Instant.now())
taskDao.updateTask(updated.toEntity())
}
override suspend fun deleteTask(taskId: Long, isDeleted: Boolean) {
taskDao.markTaskDeleted(taskId, isDeleted)
}
override suspend fun closeTask(taskId: Long, isDone: Boolean) {
taskDao.markTaskDone(taskId, isDone)
}
override suspend fun increaseTaskCycle(taskId: Long) {
taskDao.increaseCycle(taskId)
}
override fun getTaskLists(): Flow<List<TaskList>> {
return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }}
}
override suspend fun insertTaskList(taskList: TaskList) {
taskListDao.insertTaskList(taskList.toEntity())
}
override suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean) {
taskDao.deleteAllTasksFromList(taskListId, isDeleted)
taskListDao.deleteTaskList(taskListId, isDeleted)
}
}

View File

@@ -0,0 +1,14 @@
package com.wismna.geoffroy.donext.domain.model
import java.time.Instant
data class Task(
val id: Long,
val name: String,
val description: String,
val cycles: Int,
val isDone: Boolean,
val isDeleted: Boolean,
val taskListId: Long,
val updateDate: Instant = Instant.now()
)

View File

@@ -0,0 +1,7 @@
package com.wismna.geoffroy.donext.domain.model
data class TaskList(
val id: Long,
val name: String,
val isDeleted: Boolean
)

View File

@@ -0,0 +1,18 @@
package com.wismna.geoffroy.donext.domain.repository
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.model.TaskList
import kotlinx.coroutines.flow.Flow
interface TaskRepository {
suspend fun getTasksForList(listId: Long): List<Task>
suspend fun insertTask(task: Task)
suspend fun updateTask(task: Task)
suspend fun deleteTask(taskId: Long, isDeleted: Boolean)
suspend fun closeTask(taskId: Long, isDone: Boolean)
suspend fun increaseTaskCycle(taskId: Long)
fun getTaskLists(): Flow<List<TaskList>>
suspend fun insertTaskList(taskList: TaskList)
suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean)
}

View File

@@ -0,0 +1,10 @@
package com.wismna.geoffroy.donext.domain.usecase
import com.wismna.geoffroy.donext.domain.model.TaskList
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetTaskListsUseCase @Inject constructor(private val repository: TaskRepository) {
operator fun invoke(): Flow<List<TaskList>> = repository.getTaskLists()
}

View File

@@ -0,0 +1,114 @@
package com.wismna.geoffroy.donext.presentation
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.wismna.geoffroy.donext.domain.model.TaskList
import com.wismna.geoffroy.donext.presentation.screen.TaskListScreen
import com.wismna.geoffroy.donext.presentation.ui.theme.DoNextTheme
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
DoNextTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
val viewModel: MainViewModel = hiltViewModel<MainViewModel>()
MainScreen(
viewModel,
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(viewModel: MainViewModel, modifier: Modifier = Modifier) {
val navController = rememberNavController()
if (viewModel.isLoading) {
// Show loading or empty state
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
val startDestination = viewModel.taskLists[0]
// TODO: get last opened tab from saved settings
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
Scaffold(modifier = modifier) { contentPadding ->
PrimaryTabRow(
selectedTabIndex = selectedDestination,
modifier = Modifier.padding(contentPadding)
) {
viewModel.taskLists.forEachIndexed { index, destination ->
Tab(
selected = selectedDestination == index,
onClick = {
navController.navigate(route = destination.name)
selectedDestination = index
},
text = {
Text(
text = destination.name,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
AppNavHost(navController, startDestination, viewModel)
}
}
}
@Composable
fun AppNavHost(
navController: NavHostController,
startDestination: TaskList,
viewModel: MainViewModel,
modifier: Modifier = Modifier
) {
NavHost(
navController,
startDestination = startDestination.name
) {
viewModel.taskLists.forEach { destination ->
composable(destination.name) {
TaskListScreen(destination, modifier)
}
}
}
}

View File

@@ -0,0 +1,19 @@
package com.wismna.geoffroy.donext.presentation.screen
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.wismna.geoffroy.donext.domain.model.TaskList
@Composable
fun TaskListScreen(taskList: TaskList, modifier: Modifier = Modifier) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("${taskList.name} Screen")
}
}

View File

@@ -0,0 +1,11 @@
package com.wismna.geoffroy.donext.presentation.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -0,0 +1,57 @@
package com.wismna.geoffroy.donext.presentation.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun DoNextTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,34 @@
package com.wismna.geoffroy.donext.presentation.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@@ -0,0 +1,34 @@
package com.wismna.geoffroy.donext.presentation.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.TaskList
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
getTaskLists: GetTaskListsUseCase
) : ViewModel() {
var taskLists by mutableStateOf<List<TaskList>>(emptyList())
private set
var isLoading by mutableStateOf(true)
private set
init {
getTaskLists()
.onEach { lists ->
taskLists = lists
isLoading = false
}
.launchIn(viewModelScope)
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#303F9F</color>
</resources>

View File

@@ -0,0 +1,4 @@
<resources>
<string name="app_name">DoNext</string>
<string name="title_activity_main">MainActivity</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.DoNext" parent="android:Theme.Material.Light.NoActionBar" />
</resources>