├── .idea
├── .name
├── .gitignore
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── compiler.xml
├── vcs.xml
├── misc.xml
├── jarRepositories.xml
└── gradle.xml
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── layout
│ │ │ │ └── activity_main.xml
│ │ │ ├── navigation
│ │ │ │ ├── app_nav_graph.xml
│ │ │ │ └── details_nav_graph.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── todo
│ │ │ │ └── app
│ │ │ │ ├── TodoApplication.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── di
│ │ │ │ ├── NavigationModule.kt
│ │ │ │ ├── AggregatorModule.kt
│ │ │ │ ├── DataModule.kt
│ │ │ │ └── RouterBindingsModule.kt
│ │ │ │ ├── data
│ │ │ │ └── AppDatabase.kt
│ │ │ │ └── navigation
│ │ │ │ ├── AppRouter.kt
│ │ │ │ ├── DetailsRouter.kt
│ │ │ │ └── BaseRouter.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── todo
│ │ │ └── app
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── todo
│ │ └── app
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── libs
└── base
│ ├── data
│ ├── consumer-rules.pro
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── todo
│ │ │ │ └── base
│ │ │ │ └── data
│ │ │ │ ├── cache
│ │ │ │ └── Cache.kt
│ │ │ │ └── dao
│ │ │ │ └── BaseDao.kt
│ │ ├── test
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── todo
│ │ │ │ └── base
│ │ │ │ └── data
│ │ │ │ └── ExampleUnitTest.kt
│ │ └── androidTest
│ │ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── todo
│ │ │ └── base
│ │ │ └── data
│ │ │ └── ExampleInstrumentedTest.kt
│ ├── proguard-rules.pro
│ └── build.gradle
│ ├── view
│ ├── consumer-rules.pro
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── java
│ │ │ │ └── com
│ │ │ │ │ └── example
│ │ │ │ │ └── todo
│ │ │ │ │ └── base
│ │ │ │ │ └── view
│ │ │ │ │ ├── navigation
│ │ │ │ │ ├── route
│ │ │ │ │ │ └── IRoute.kt
│ │ │ │ │ ├── router
│ │ │ │ │ │ ├── IRouter.kt
│ │ │ │ │ │ └── IArgsRouter.kt
│ │ │ │ │ ├── fragment
│ │ │ │ │ │ ├── IRoutedViewModel.kt
│ │ │ │ │ │ ├── RoutedViewModelDelegate.kt
│ │ │ │ │ │ └── StackFragment.kt
│ │ │ │ │ └── exception
│ │ │ │ │ │ └── IllegalRouteException.kt
│ │ │ │ │ └── lifecycle
│ │ │ │ │ └── LiveDataEvent.kt
│ │ │ ├── AndroidManifest.xml
│ │ │ └── res
│ │ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ └── themes.xml
│ │ │ │ └── values-night
│ │ │ │ └── themes.xml
│ │ ├── test
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── todo
│ │ │ │ └── base
│ │ │ │ └── view
│ │ │ │ └── ExampleUnitTest.kt
│ │ └── androidTest
│ │ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── todo
│ │ │ └── base
│ │ │ └── view
│ │ │ └── ExampleInstrumentedTest.kt
│ ├── proguard-rules.pro
│ └── build.gradle
│ └── domain
│ ├── .gitignore
│ ├── src
│ └── main
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── todo
│ │ └── base
│ │ ├── domain
│ │ ├── IExceptionHandler.kt
│ │ ├── FlowUseCaseExtensions.kt
│ │ ├── FlowUseCase.kt
│ │ └── UseCase.kt
│ │ └── data
│ │ ├── ITimeLimitedResource.kt
│ │ ├── RefreshControl.kt
│ │ ├── Resource.kt
│ │ └── ResourceGroup.kt
│ └── build.gradle
├── features
└── todos
│ ├── data
│ ├── consumer-rules.pro
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── todo
│ │ │ │ └── todos
│ │ │ │ └── data
│ │ │ │ ├── local
│ │ │ │ ├── entity
│ │ │ │ │ └── TodoEntity.kt
│ │ │ │ ├── mappings
│ │ │ │ │ └── TodoMappings.kt
│ │ │ │ ├── dao
│ │ │ │ │ └── TodoDao.kt
│ │ │ │ └── TodosCache.kt
│ │ │ │ ├── di
│ │ │ │ └── TodoDataModule.kt
│ │ │ │ └── remote
│ │ │ │ └── MockTodosApi.kt
│ │ ├── test
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── todo
│ │ │ │ └── todos
│ │ │ │ └── data
│ │ │ │ └── ExampleUnitTest.kt
│ │ └── androidTest
│ │ │ └── java
│ │ │ └── com
│ │ │ └── example
│ │ │ └── todo
│ │ │ └── todos
│ │ │ └── data
│ │ │ └── ExampleInstrumentedTest.kt
│ ├── proguard-rules.pro
│ └── build.gradle
│ ├── view
│ ├── consumer-rules.pro
│ ├── .gitignore
│ ├── src
│ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── todo
│ │ │ │ └── todos
│ │ │ │ └── view
│ │ │ │ ├── form
│ │ │ │ ├── ITodoFormRouter.kt
│ │ │ │ ├── TodoFormFragment.kt
│ │ │ │ └── TodoFormViewModel.kt
│ │ │ │ ├── details
│ │ │ │ ├── dialog
│ │ │ │ │ ├── IDeleteTodoRouter.kt
│ │ │ │ │ └── DeleteTodoDialog.kt
│ │ │ │ ├── ITodoDetailsRouter.kt
│ │ │ │ ├── TodoDetailsFragment.kt
│ │ │ │ └── TodoDetailsViewModel.kt
│ │ │ │ ├── list
│ │ │ │ ├── ITodoListRouter.kt
│ │ │ │ ├── TodoListAdapter.kt
│ │ │ │ ├── TodoListFragment.kt
│ │ │ │ └── TodoListViewModel.kt
│ │ │ │ └── di
│ │ │ │ └── TodoFragmentModule.kt
│ │ │ └── res
│ │ │ ├── menu
│ │ │ └── menu_todo_list.xml
│ │ │ ├── values
│ │ │ └── strings.xml
│ │ │ └── layout
│ │ │ ├── list_item_todo.xml
│ │ │ ├── fragment_todo_list.xml
│ │ │ ├── dialog_delete_todo.xml
│ │ │ ├── fragment_todo_form.xml
│ │ │ └── fragment_todo_details.xml
│ ├── proguard-rules.pro
│ └── build.gradle
│ └── domain
│ ├── .gitignore
│ ├── src
│ └── main
│ │ └── java
│ │ └── com
│ │ └── example
│ │ └── todo
│ │ └── todos
│ │ ├── data
│ │ ├── ITodoCache.kt
│ │ └── ITodoApi.kt
│ │ └── domain
│ │ ├── model
│ │ └── Todo.kt
│ │ ├── usecase
│ │ ├── AddTodoUseCase.kt
│ │ ├── DeleteTodoUseCase.kt
│ │ ├── GetAllTodosUseCase.kt
│ │ ├── SetTodoCompletionUseCase.kt
│ │ └── GetTodoByIdUseCase.kt
│ │ ├── di
│ │ └── TodoUseCaseModule.kt
│ │ └── repository
│ │ └── TodoRepository.kt
│ └── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── README.md
├── gradle.properties
├── gradlew.bat
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | Todo
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/libs/base/data/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libs/base/view/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/features/todos/data/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/features/todos/view/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/libs/base/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/libs/base/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/libs/base/view/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/features/todos/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/features/todos/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/features/todos/view/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | TodoApp
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aleweichandt/todo-android-app/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/libs/base/domain/src/main/java/com/example/todo/base/domain/IExceptionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.domain
2 |
3 | interface IExceptionHandler {
4 | fun handle(t: Throwable)
5 | }
6 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/java/com/example/todo/base/view/navigation/route/IRoute.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.view.navigation.route
2 |
3 | interface IRoute {
4 | object Back : IRoute
5 | }
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/libs/base/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/features/todos/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/features/todos/view/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/todo/app/TodoApplication.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.app
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class TodoApplication : Application()
8 |
--------------------------------------------------------------------------------
/features/todos/view/src/main/java/com/example/todo/todos/view/form/ITodoFormRouter.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.view.form
2 |
3 | import com.example.todo.base.view.navigation.router.IRouter
4 |
5 | interface ITodoFormRouter : IRouter
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jan 30 20:58:37 CET 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
7 |
--------------------------------------------------------------------------------
/features/todos/view/src/main/java/com/example/todo/todos/view/details/dialog/IDeleteTodoRouter.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.view.details.dialog
2 |
3 | import androidx.fragment.app.Fragment
4 |
5 | interface IDeleteTodoRouter {
6 | fun popWithResult(instance: Fragment, result: Boolean)
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/java/com/example/todo/base/view/navigation/router/IRouter.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.view.navigation.router
2 |
3 | import androidx.fragment.app.Fragment
4 | import com.example.todo.base.view.navigation.route.IRoute
5 |
6 | interface IRouter {
7 | fun pop(instance: Fragment)
8 | }
9 |
--------------------------------------------------------------------------------
/libs/base/domain/src/main/java/com/example/todo/base/data/ITimeLimitedResource.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.data
2 |
3 | import java.util.*
4 |
5 | interface ITimeLimitedResource {
6 | var refreshRate: Long
7 | val lastUpdate: Date?
8 |
9 | suspend fun evict(cleanup: Boolean = false)
10 | }
11 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/java/com/example/todo/base/view/navigation/router/IArgsRouter.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.view.navigation.router
2 |
3 | import androidx.fragment.app.Fragment
4 | import java.io.Serializable
5 |
6 | interface IArgsRouter {
7 | fun args(instance: Fragment): Args
8 | }
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/features/todos/view/src/main/res/menu/menu_todo_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/features/todos/data/src/main/java/com/example/todo/todos/data/local/entity/TodoEntity.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.data.local.entity
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "todo")
7 | data class TodoEntity(
8 | @PrimaryKey val uuid: Long,
9 | val value: String,
10 | val body: String,
11 | val completed: Boolean,
12 | )
13 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/java/com/example/todo/base/view/navigation/fragment/IRoutedViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.view.navigation.fragment
2 |
3 | import androidx.lifecycle.LiveData
4 | import com.example.todo.base.view.lifecycle.LiveDataEvent
5 | import com.example.todo.base.view.navigation.route.IRoute
6 |
7 | interface IRoutedViewModel {
8 | val route: LiveData>
9 | }
10 |
--------------------------------------------------------------------------------
/features/todos/data/src/main/java/com/example/todo/todos/data/local/mappings/TodoMappings.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.data.local
2 |
3 | import com.example.todo.todos.data.local.entity.TodoEntity
4 | import com.example.todo.todos.domain.model.Todo
5 |
6 | fun Todo.toEntity() = TodoEntity(
7 | uuid, value, body, completed
8 | )
9 |
10 | fun TodoEntity.toDomain() = Todo(
11 | value, body, completed, uuid
12 | )
13 |
--------------------------------------------------------------------------------
/features/todos/domain/src/main/java/com/example/todo/todos/data/ITodoCache.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.data
2 |
3 | import com.example.todo.todos.domain.model.Todo
4 |
5 | interface ITodoCache {
6 | suspend fun getAllTodos(): List?
7 | suspend fun getTodoById(id: Long): Todo?
8 | suspend fun storeAllTodos(todos: List)
9 | suspend fun storeTodo(todo: Todo)
10 | suspend fun deleteAllTodos()
11 | }
12 |
--------------------------------------------------------------------------------
/libs/base/domain/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | id 'kotlin'
4 | }
5 |
6 | java {
7 | sourceCompatibility = JavaVersion.VERSION_1_8
8 | targetCompatibility = JavaVersion.VERSION_1_8
9 | }
10 |
11 | dependencies {
12 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
13 |
14 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/java/com/example/todo/base/view/navigation/exception/IllegalRouteException.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.view.navigation.exception
2 |
3 | import androidx.fragment.app.Fragment
4 | import com.example.todo.base.view.navigation.route.IRoute
5 |
6 | class IllegalRouteException(instance: Fragment, route: IRoute) : IllegalStateException(
7 | "unknown route: ${route.javaClass.simpleName} for instance: ${instance.javaClass.simpleName}"
8 | )
9 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/todo/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.app
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import dagger.hilt.android.AndroidEntryPoint
6 |
7 | @AndroidEntryPoint
8 | class MainActivity : AppCompatActivity() {
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 | setContentView(R.layout.activity_main)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/todo/app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.app
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/features/todos/domain/src/main/java/com/example/todo/todos/data/ITodoApi.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.data
2 |
3 | import com.example.todo.todos.domain.model.Todo
4 |
5 | interface ITodoApi {
6 | suspend fun getAllTodos(): List?
7 | suspend fun getTodoById(id: Long): Todo?
8 | suspend fun addTodo(todo: Todo): Boolean?
9 | suspend fun deleteTodo(todo: Todo): Boolean?
10 | suspend fun updateTodo(todo: Todo, update: Todo): Boolean?
11 | }
12 |
--------------------------------------------------------------------------------
/libs/base/data/src/main/java/com/example/todo/base/data/cache/Cache.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.data.cache
2 |
3 | import com.example.todo.base.domain.IExceptionHandler
4 |
5 | abstract class Cache(
6 | private val exceptionHandler: IExceptionHandler
7 | ) {
8 | protected suspend fun runQuery(query: suspend () -> T) =
9 | query.runCatching { invoke() }
10 | .onFailure { exceptionHandler.handle(it) }
11 | .getOrNull()
12 | }
13 |
--------------------------------------------------------------------------------
/libs/base/data/src/test/java/com/example/todo/base/data/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.data
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/libs/base/view/src/test/java/com/example/todo/base/view/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.view
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/features/todos/data/src/test/java/com/example/todo/todos/data/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.data
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/features/todos/domain/src/main/java/com/example/todo/todos/domain/model/Todo.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.domain.model
2 |
3 | import java.io.Serializable
4 |
5 | data class Todo(
6 | val value: String = "",
7 | val body: String = "",
8 | val completed: Boolean = false,
9 | val uuid: Long = nextUuid
10 | ) : Serializable {
11 | companion object {
12 | private var uuid: Long = 0
13 | val nextUuid: Long
14 | get() = uuid.also { uuid = it + 1 }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/features/todos/view/src/main/java/com/example/todo/todos/view/list/ITodoListRouter.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.view.list
2 |
3 | import androidx.fragment.app.Fragment
4 | import com.example.todo.base.view.navigation.route.IRoute
5 | import com.example.todo.base.view.navigation.router.IRouter
6 |
7 | interface ITodoListRouter : IRouter {
8 | sealed class TodoListRoute : IRoute {
9 | data class DetailsRoute(val uuid: Long) : TodoListRoute()
10 | object FormRoute : TodoListRoute()
11 | }
12 |
13 | fun push(instance: Fragment, route: TodoListRoute)
14 | }
15 |
--------------------------------------------------------------------------------
/features/todos/domain/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java-library'
3 | id 'kotlin'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | java {
8 | sourceCompatibility = JavaVersion.VERSION_1_8
9 | targetCompatibility = JavaVersion.VERSION_1_8
10 | }
11 |
12 | dependencies {
13 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
14 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
15 |
16 | // Dagger
17 | implementation 'com.google.dagger:dagger:2.31.2'
18 | kapt 'com.google.dagger:dagger-compiler:2.31.2'
19 |
20 | api project(':baseDomain')
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/todo/app/di/NavigationModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.app.di
2 |
3 | import com.example.todo.app.navigation.AppRouter
4 | import com.example.todo.app.navigation.DetailsRouter
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | object NavigationModule {
14 | @Provides
15 | @Singleton
16 | fun provideAppRouter() = AppRouter()
17 |
18 | @Provides
19 | @Singleton
20 | fun provideDetailsRouter() = DetailsRouter()
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/todo/app/di/AggregatorModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.app.di
2 |
3 | import com.example.todo.base.domain.IExceptionHandler
4 | import com.example.todo.todos.domain.di.TodoUseCaseModule
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module(includes = [TodoUseCaseModule::class])
12 | @InstallIn(SingletonComponent::class)
13 | object AggregatorModule {
14 | @Provides
15 | @Singleton
16 | fun provideExceptionHandle() = object : IExceptionHandler {
17 | override fun handle(t: Throwable) {}
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/todo/app/di/DataModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.app.di
2 |
3 | import android.app.Application
4 | import com.example.todo.app.data.AppDatabase
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | object DataModule {
14 | @Provides
15 | @Singleton
16 | fun provideAppDatabase(application: Application) = AppDatabase.Builder(application).build()
17 |
18 | @Provides
19 | @Singleton
20 | fun provideTodoDao(database: AppDatabase) = database.todoDao()
21 | }
22 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app',
2 | ':baseDomain',
3 | ':baseView',
4 | ':baseData',
5 | ':todosDomain',
6 | ':todosView',
7 | ':todosData'
8 | rootProject.name = "Todo"
9 |
10 | // Libs
11 | // Base
12 | project(':baseDomain').projectDir = new File('libs/base/domain')
13 | project(':baseView').projectDir = new File('libs/base/view')
14 | project(':baseData').projectDir = new File('libs/base/data')
15 |
16 | // Features
17 | // Todos
18 | project(':todosDomain').projectDir = new File('features/todos/domain')
19 | project(':todosView').projectDir = new File('features/todos/view')
20 | project(':todosData').projectDir = new File('features/todos/data')
21 |
--------------------------------------------------------------------------------
/libs/base/domain/src/main/java/com/example/todo/base/domain/FlowUseCaseExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.domain
2 |
3 | import kotlinx.coroutines.ExperimentalCoroutinesApi
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.filter
6 | import kotlinx.coroutines.flow.first
7 | import kotlinx.coroutines.flow.toList
8 |
9 |
10 | @ExperimentalCoroutinesApi
11 | suspend operator fun FlowUseCase.invoke() = this(Unit)
12 |
13 | suspend fun Flow>.resolve() =
14 | latest { it is UseCase.Result.Success } ?: first()
15 |
16 | suspend fun Flow.latest(isValid: (T?) -> Boolean = { it != null }): T? =
17 | filter { isValid(it) }.toList().lastOrNull()
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Todo Android App Example
2 |
3 | This example aims to showcase some android patterns i've used across projects i've been worked in.
4 |
5 | ### Showcase
6 |
7 | * Clean architecture with kotlin domain modules
8 |
9 | * Multi module android project
10 |
11 | * Androidx navigation on multi module android projects
12 |
13 | * Offline support using Room Database and kotlin flow **(TODO)**
14 |
15 | ## Contributing
16 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
17 |
18 | Please make sure to update tests as appropriate.
19 |
20 | ## Special Thanks
21 | All [Sync. Money](https://www.sync.money/) Android Team
22 |
23 | ## License
24 | [MIT](https://choosealicense.com/licenses/mit/)
25 |
--------------------------------------------------------------------------------
/features/todos/view/src/main/java/com/example/todo/todos/view/details/ITodoDetailsRouter.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.view.details
2 |
3 | import androidx.fragment.app.Fragment
4 | import com.example.todo.base.view.navigation.route.IRoute
5 | import com.example.todo.base.view.navigation.router.IArgsRouter
6 | import com.example.todo.base.view.navigation.router.IRouter
7 | import java.io.Serializable
8 |
9 | interface ITodoDetailsRouter : IRouter, IArgsRouter {
10 | sealed class TodoDetailsRoute : IRoute {
11 | object DeleteTodoDialog : TodoDetailsRoute()
12 | }
13 |
14 | data class TodoDetailsArgs(val uuid: Long) : Serializable
15 |
16 | fun pushForResult(instance: Fragment, route: TodoDetailsRoute, callback: (Boolean) -> Unit)
17 | }
18 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/java/com/example/todo/base/view/navigation/fragment/RoutedViewModelDelegate.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.view.navigation.fragment
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.example.todo.base.view.lifecycle.LiveDataEvent
6 | import com.example.todo.base.view.navigation.route.IRoute
7 |
8 | class RoutedViewModelDelegate : IRoutedViewModel {
9 |
10 | private val _route = MutableLiveData>()
11 | override val route: LiveData>
12 | get() = _route
13 |
14 | fun pushRoute(route: Route) {
15 | _route.value = LiveDataEvent(route)
16 | }
17 |
18 | fun popRoute() {
19 | _route.value = LiveDataEvent(IRoute.Back)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/features/todos/data/src/main/java/com/example/todo/todos/data/local/dao/TodoDao.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.data.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Query
5 | import androidx.room.Transaction
6 | import com.example.todo.base.data.dao.BaseDao
7 | import com.example.todo.todos.data.local.entity.TodoEntity
8 |
9 | @Dao
10 | interface TodoDao : BaseDao {
11 | @Query("SELECT * from todo")
12 | suspend fun getAll(): Array
13 |
14 | @Query("SELECT * from todo WHERE :uuid = uuid")
15 | suspend fun getById(uuid: Long): TodoEntity?
16 |
17 | @Query("DELETE from todo")
18 | suspend fun deleteAll()
19 | }
20 |
21 | @Transaction
22 | suspend fun TodoDao.replaceAll(vararg todos: TodoEntity) {
23 | deleteAll()
24 | insert(*todos)
25 | }
26 |
--------------------------------------------------------------------------------
/features/todos/view/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | My TODOs
3 | Add new TODO
4 | Details
5 | Title
6 | Body
7 | Add
8 | Submit
9 | Delete
10 | Yes
11 | No
12 | Delete todo
13 | You are about to delete this TODO.\nAre you sure you want to continue?
14 |
15 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/todo/app/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.app
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.example.todo.app", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/features/todos/domain/src/main/java/com/example/todo/todos/domain/usecase/AddTodoUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.domain.usecase
2 |
3 | import com.example.todo.base.domain.IExceptionHandler
4 | import com.example.todo.base.domain.UseCase
5 | import com.example.todo.todos.domain.model.Todo
6 | import com.example.todo.todos.domain.repository.TodoRepository
7 | import kotlinx.coroutines.CoroutineDispatcher
8 | import kotlinx.coroutines.Dispatchers
9 |
10 | class AddTodoUseCase(
11 | private val repository: TodoRepository,
12 | handler: IExceptionHandler,
13 | dispatcher: CoroutineDispatcher = Dispatchers.IO
14 | ) : UseCase(handler, dispatcher) {
15 | override suspend fun performAction(param: Todo): Result =
16 | repository.addTodo(param)?.let { Result.Success(it) } ?: Result.Failure()
17 | }
18 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/features/todos/domain/src/main/java/com/example/todo/todos/domain/usecase/DeleteTodoUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.domain.usecase
2 |
3 | import com.example.todo.base.domain.IExceptionHandler
4 | import com.example.todo.base.domain.UseCase
5 | import com.example.todo.todos.domain.model.Todo
6 | import com.example.todo.todos.domain.repository.TodoRepository
7 | import kotlinx.coroutines.CoroutineDispatcher
8 | import kotlinx.coroutines.Dispatchers
9 |
10 | class DeleteTodoUseCase(
11 | private val repository: TodoRepository,
12 | handler: IExceptionHandler,
13 | dispatcher: CoroutineDispatcher = Dispatchers.IO
14 | ) : UseCase(handler, dispatcher) {
15 | override suspend fun performAction(param: Todo): Result =
16 | repository.deleteTodo(param)?.let { Result.Success(it) } ?: Result.Failure()
17 | }
18 |
--------------------------------------------------------------------------------
/libs/base/data/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/libs/base/view/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/features/todos/data/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/features/todos/data/src/androidTest/java/com/example/todo/todos/data/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.data
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.example.todo.todos.data", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/features/todos/view/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/libs/base/data/src/androidTest/java/com/example/todo/base/data/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.data
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.example.todo.base.data.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/libs/base/view/src/androidTest/java/com/example/todo/base/view/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.view
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.example.todo.base.view.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
--------------------------------------------------------------------------------
/libs/base/domain/src/main/java/com/example/todo/base/domain/FlowUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.domain
2 |
3 | import com.example.todo.base.domain.UseCase.Result
4 | import kotlinx.coroutines.CoroutineDispatcher
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.flow.*
7 |
8 | abstract class FlowUseCase(
9 | private val exceptionHandler: IExceptionHandler,
10 | private val dispatcher: CoroutineDispatcher
11 | ) {
12 |
13 | @ExperimentalCoroutinesApi
14 | @Suppress("TooGenericExceptionCaught")
15 | suspend operator fun invoke(param: TParam) =
16 | performAction(param)
17 | .catch { exception ->
18 | exceptionHandler.handle(exception)
19 | emit(Result.Failure(exception))
20 | }
21 | .flowOn(dispatcher)
22 |
23 | protected abstract suspend fun performAction(param: TParam): Flow>
24 | }
25 |
--------------------------------------------------------------------------------
/features/todos/data/src/main/java/com/example/todo/todos/data/di/TodoDataModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.data.di
2 |
3 | import com.example.todo.base.domain.IExceptionHandler
4 | import com.example.todo.todos.data.ITodoApi
5 | import com.example.todo.todos.data.ITodoCache
6 | import com.example.todo.todos.data.local.TodosCache
7 | import com.example.todo.todos.data.local.dao.TodoDao
8 | import com.example.todo.todos.data.remote.MockTodosApi
9 | import dagger.Module
10 | import dagger.Provides
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.components.SingletonComponent
13 | import javax.inject.Singleton
14 |
15 | @Module
16 | @InstallIn(SingletonComponent::class)
17 | object TodoDataModule {
18 | @Provides
19 | @Singleton
20 | fun provideTodosApi(): ITodoApi = MockTodosApi()
21 |
22 | @Provides
23 | @Singleton
24 | fun provideTodosCache(dao: TodoDao, handler: IExceptionHandler): ITodoCache =
25 | TodosCache(dao, handler)
26 | }
27 |
--------------------------------------------------------------------------------
/features/todos/domain/src/main/java/com/example/todo/todos/domain/usecase/GetAllTodosUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.domain.usecase
2 |
3 | import com.example.todo.base.domain.FlowUseCase
4 | import com.example.todo.base.domain.IExceptionHandler
5 | import com.example.todo.base.domain.UseCase.Result
6 | import com.example.todo.todos.domain.model.Todo
7 | import com.example.todo.todos.domain.repository.TodoRepository
8 | import kotlinx.coroutines.CoroutineDispatcher
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.map
12 |
13 | class GetAllTodosUseCase(
14 | private val repository: TodoRepository,
15 | handler: IExceptionHandler,
16 | dispatcher: CoroutineDispatcher = Dispatchers.IO
17 | ) : FlowUseCase>(handler, dispatcher) {
18 | override suspend fun performAction(param: Boolean): Flow>> =
19 | repository.getAllTodos(param).map { Result.fromNullable(it) }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/todo/app/data/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.app.data
2 |
3 | import android.app.Application
4 | import androidx.room.Database
5 | import androidx.room.Room
6 | import androidx.room.RoomDatabase
7 | import com.example.todo.todos.data.local.dao.TodoDao
8 | import com.example.todo.todos.data.local.entity.TodoEntity
9 |
10 | private const val DATABASE_VERSION = 1
11 | private const val DATABASE_NAME = "todo-database"
12 |
13 | @Database(
14 | entities = [TodoEntity::class],
15 | version = DATABASE_VERSION,
16 | exportSchema = false
17 | )
18 | abstract class AppDatabase : RoomDatabase() {
19 | class Builder(private val application: Application) {
20 | private val builder: RoomDatabase.Builder
21 | get() = Room.databaseBuilder(application, AppDatabase::class.java, DATABASE_NAME)
22 | .fallbackToDestructiveMigration()
23 |
24 | fun build(): AppDatabase = builder.build()
25 | }
26 |
27 | abstract fun todoDao(): TodoDao
28 | }
29 |
--------------------------------------------------------------------------------
/features/todos/domain/src/main/java/com/example/todo/todos/domain/usecase/SetTodoCompletionUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.domain.usecase
2 |
3 | import com.example.todo.base.domain.IExceptionHandler
4 | import com.example.todo.base.domain.UseCase
5 | import com.example.todo.todos.domain.model.Todo
6 | import com.example.todo.todos.domain.repository.TodoRepository
7 | import kotlinx.coroutines.CoroutineDispatcher
8 | import kotlinx.coroutines.Dispatchers
9 |
10 | class SetTodoCompletionUseCase(
11 | private val repository: TodoRepository,
12 | handler: IExceptionHandler,
13 | dispatcher: CoroutineDispatcher = Dispatchers.IO
14 | ) : UseCase(handler, dispatcher) {
15 | data class Request(
16 | val item: Todo,
17 | val completion: Boolean
18 | )
19 |
20 | override suspend fun performAction(param: Request) =
21 | repository.updateTodo(param.item, param.item.copy(completed = param.completion))
22 | ?.let { Result.Success(it) } ?: Result.Failure()
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/features/todos/domain/src/main/java/com/example/todo/todos/domain/usecase/GetTodoByIdUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.domain.usecase
2 |
3 | import com.example.todo.base.domain.FlowUseCase
4 | import com.example.todo.base.domain.IExceptionHandler
5 | import com.example.todo.base.domain.UseCase.Result
6 | import com.example.todo.todos.domain.model.Todo
7 | import com.example.todo.todos.domain.repository.TodoRepository
8 | import kotlinx.coroutines.CoroutineDispatcher
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.map
11 |
12 | class GetTodoByIdUseCase(
13 | private val repository: TodoRepository,
14 | handler: IExceptionHandler,
15 | dispatcher: CoroutineDispatcher = Dispatchers.IO
16 | ) : FlowUseCase(handler, dispatcher) {
17 | data class Request(
18 | val uuid: Long,
19 | val force: Boolean = false
20 | )
21 | override suspend fun performAction(param: Request) =
22 | repository.getTodoById(param.uuid, param.force).map { Result.fromNullable(it) }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/todo/app/di/RouterBindingsModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.app.di
2 |
3 | import com.example.todo.app.navigation.AppRouter
4 | import com.example.todo.app.navigation.DetailsRouter
5 | import com.example.todo.todos.view.details.ITodoDetailsRouter
6 | import com.example.todo.todos.view.details.dialog.IDeleteTodoRouter
7 | import com.example.todo.todos.view.form.ITodoFormRouter
8 | import com.example.todo.todos.view.list.ITodoListRouter
9 | import dagger.Binds
10 | import dagger.Module
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.android.components.ActivityComponent
13 |
14 | @Module
15 | @InstallIn(ActivityComponent::class)
16 | abstract class RouterBindingsModule {
17 | @Binds
18 | abstract fun bindTodoListRouter(appRouter: AppRouter): ITodoListRouter
19 |
20 | @Binds
21 | abstract fun bindTodoFormRouter(appRouter: AppRouter): ITodoFormRouter
22 |
23 | @Binds
24 | abstract fun bindTodoDetailsRouter(detailsRouter: DetailsRouter): ITodoDetailsRouter
25 |
26 | @Binds
27 | abstract fun bindDeleteTodoRouter(detailsRouter: DetailsRouter): IDeleteTodoRouter
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/java/com/example/todo/base/view/navigation/fragment/StackFragment.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.view.navigation.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.fragment.app.Fragment
6 | import com.example.todo.base.view.lifecycle.consume
7 | import com.example.todo.base.view.navigation.exception.IllegalRouteException
8 | import com.example.todo.base.view.navigation.route.IRoute
9 | import com.example.todo.base.view.navigation.router.IRouter
10 |
11 | abstract class StackFragment : Fragment() {
12 | abstract val router: IRouter
13 |
14 | protected abstract val viewModel: IRoutedViewModel
15 |
16 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
17 | super.onViewCreated(view, savedInstanceState)
18 | observeRoute()
19 | }
20 |
21 | private fun observeRoute() {
22 | viewModel.route.consume(viewLifecycleOwner, this::navigate)
23 | }
24 |
25 | open fun navigate(route: IRoute) {
26 | when (route) {
27 | is IRoute.Back -> router.pop(this)
28 | else -> throw IllegalRouteException(this, route)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/features/todos/view/src/main/java/com/example/todo/todos/view/details/dialog/DeleteTodoDialog.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.view.details.dialog
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.DialogFragment
8 | import com.example.todo.todos.view.databinding.DialogDeleteTodoBinding
9 | import dagger.hilt.android.AndroidEntryPoint
10 | import javax.inject.Inject
11 |
12 | @AndroidEntryPoint
13 | class DeleteTodoDialog : DialogFragment() {
14 | @Inject
15 | lateinit var router: IDeleteTodoRouter
16 |
17 | override fun onCreateView(
18 | inflater: LayoutInflater, container: ViewGroup?,
19 | savedInstanceState: Bundle?
20 | ): View? {
21 | val bindings = DialogDeleteTodoBinding.inflate(inflater, container, false)
22 | bindings.lifecycleOwner = viewLifecycleOwner
23 | bindings.accept.setOnClickListener { onResult(true) }
24 | bindings.dismiss.setOnClickListener { onResult(false) }
25 | return bindings.root
26 | }
27 |
28 | private fun onResult(result: Boolean) {
29 | router.popWithResult(this, result)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/libs/base/domain/src/main/java/com/example/todo/base/domain/UseCase.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.domain
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.withContext
5 |
6 | abstract class UseCase(
7 | private val exceptionHandler: IExceptionHandler,
8 | private val dispatcher: CoroutineDispatcher
9 | ) {
10 | sealed class Result {
11 | companion object {
12 | fun fromNullable(result: TResultModel?) = when (result) {
13 | null -> Failure()
14 | else -> Success(result)
15 | }
16 | }
17 |
18 | data class Success(val result: TResultModel) : Result()
19 | data class Failure(val error: Throwable? = null) : Result()
20 | }
21 |
22 | suspend operator fun invoke(param: TParam) = try {
23 | withContext(dispatcher) {
24 | performAction(param)
25 | }
26 | } catch (error: Throwable) {
27 | exceptionHandler.handle(error)
28 | Result.Failure(error)
29 | }
30 |
31 | protected abstract suspend fun performAction(param: TParam): Result
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/todo/app/navigation/AppRouter.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.app.navigation
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.navigation.fragment.findNavController
5 | import com.example.todo.app.DetailsNavGraphDirections
6 | import com.example.todo.app.R
7 | import com.example.todo.base.view.navigation.exception.IllegalRouteException
8 | import com.example.todo.todos.view.form.ITodoFormRouter
9 | import com.example.todo.todos.view.list.ITodoListRouter
10 | import com.example.todo.todos.view.list.ITodoListRouter.TodoListRoute
11 | import com.example.todo.todos.view.list.TodoListFragmentDirections
12 |
13 | class AppRouter : BaseRouter(), ITodoListRouter, ITodoFormRouter {
14 | override val graphId = R.id.app_nav_graph
15 |
16 | override fun push(instance: Fragment, route: TodoListRoute) {
17 | when (route) {
18 | is TodoListRoute.FormRoute -> TodoListFragmentDirections.actionToForm()
19 | is TodoListRoute.DetailsRoute -> DetailsNavGraphDirections.actionToDetails(route.uuid)
20 | else -> null
21 | }?.let {
22 | instance.findNavController().navigate(it)
23 | } ?: throw IllegalRouteException(instance, route)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/libs/base/view/src/main/java/com/example/todo/base/view/lifecycle/LiveDataEvent.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.view.lifecycle
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.LiveData
5 | import java.io.Serializable
6 |
7 | /**
8 | * Used as a wrapper for data that is exposed via a LiveData that represents an event.
9 | */
10 | open class LiveDataEvent(private val content: T) : Serializable {
11 |
12 | var hasBeenHandled = false
13 | private set // Allow external read but not write
14 |
15 | /**
16 | * Returns the content and prevents its use again.
17 | */
18 | fun consume(): T? = if (hasBeenHandled) {
19 | null
20 | } else {
21 | hasBeenHandled = true
22 | content
23 | }
24 |
25 | /**
26 | * Returns the content, even if it's already been handled.
27 | */
28 | fun peekContent(): T = content
29 | }
30 |
31 | /**
32 | * Extension that will listen for live data events and consume them right away. This allows us to
33 | * avoid writing consume on every block.
34 | */
35 | fun LiveData>.consume(owner: LifecycleOwner, onChanged: (T) -> Unit) =
36 | this.observe(owner) {
37 | it.consume()?.let { value ->
38 | onChanged(value)
39 | }
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/features/todos/view/src/main/java/com/example/todo/todos/view/di/TodoFragmentModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.view.di
2 |
3 | import com.example.todo.todos.domain.usecase.*
4 | import com.example.todo.todos.view.details.TodoDetailsViewModel
5 | import com.example.todo.todos.view.form.TodoFormViewModel
6 | import com.example.todo.todos.view.list.TodoListViewModel
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.android.components.ViewModelComponent
11 | import dagger.hilt.android.scopes.ViewModelScoped
12 |
13 | @Module
14 | @InstallIn(ViewModelComponent::class)
15 | object TodoFragmentModule {
16 | @Provides
17 | @ViewModelScoped
18 | fun bindsTodoListViewModel(
19 | getAllTodosUseCase: GetAllTodosUseCase,
20 | setTodoCompletionUseCase: SetTodoCompletionUseCase
21 | ) = TodoListViewModel(getAllTodosUseCase, setTodoCompletionUseCase)
22 |
23 | @Provides
24 | @ViewModelScoped
25 | fun bindTodoFormViewModel(
26 | addTodoUseCase: AddTodoUseCase
27 | ) = TodoFormViewModel(addTodoUseCase)
28 |
29 | @Provides
30 | @ViewModelScoped
31 | fun bind(
32 | getTodoByIdUseCase: GetTodoByIdUseCase,
33 | deleteTodoUseCase: DeleteTodoUseCase
34 | ) = TodoDetailsViewModel(getTodoByIdUseCase, deleteTodoUseCase)
35 | }
36 |
--------------------------------------------------------------------------------
/features/todos/view/src/main/java/com/example/todo/todos/view/form/TodoFormFragment.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.view.form
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.viewModels
8 | import com.example.todo.base.view.navigation.fragment.StackFragment
9 | import com.example.todo.todos.view.databinding.FragmentTodoFormBinding
10 | import dagger.hilt.android.AndroidEntryPoint
11 | import javax.inject.Inject
12 |
13 | @AndroidEntryPoint
14 | class TodoFormFragment : StackFragment() {
15 | @Inject
16 | override lateinit var router: ITodoFormRouter
17 |
18 | override val viewModel by viewModels()
19 |
20 | override fun onCreateView(
21 | inflater: LayoutInflater, container: ViewGroup?,
22 | savedInstanceState: Bundle?
23 | ): View {
24 | val bindings = FragmentTodoFormBinding.inflate(inflater, container, false)
25 | bindings.vieWModel = viewModel
26 | bindings.lifecycleOwner = viewLifecycleOwner
27 | return bindings.root
28 | }
29 |
30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
31 | super.onViewCreated(view, savedInstanceState)
32 | observeViewModel()
33 | }
34 |
35 | private fun observeViewModel() {
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
28 |
29 |
--------------------------------------------------------------------------------
/libs/base/view/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 | compileSdkVersion 30
8 | buildToolsVersion "30.0.3"
9 |
10 | defaultConfig {
11 | minSdkVersion 23
12 | targetSdkVersion 30
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | consumerProguardFiles "consumer-rules.pro"
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_1_8
28 | targetCompatibility JavaVersion.VERSION_1_8
29 | }
30 | kotlinOptions {
31 | jvmTarget = '1.8'
32 | }
33 | }
34 |
35 | dependencies {
36 |
37 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
38 | implementation "androidx.core:core-ktx:$core_version"
39 | implementation "androidx.appcompat:appcompat:$compat_version"
40 | implementation "com.google.android.material:material:$material_version"
41 |
42 | testImplementation 'junit:junit:4.+'
43 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/app_nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
15 |
22 |
25 |
26 |
31 |
32 |
--------------------------------------------------------------------------------
/libs/base/domain/src/main/java/com/example/todo/base/data/RefreshControl.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.data
2 |
3 | import java.util.*
4 | import java.util.concurrent.TimeUnit
5 |
6 | class RefreshControl(
7 | rate: Long = DEFAULT_REFRESH_RATE_MS,
8 | private var lastUpdateDate: Date? = null
9 | ) : ITimeLimitedResource {
10 | companion object {
11 | val DEFAULT_REFRESH_RATE_MS = TimeUnit.MINUTES.toMillis(5)
12 | }
13 |
14 | interface Listener {
15 | suspend fun cleanup()
16 | }
17 |
18 | private val listeners: MutableList = mutableListOf()
19 | private val children: MutableList = mutableListOf()
20 |
21 | // ITimeLimitedResource
22 | override var refreshRate: Long = rate
23 | override val lastUpdate: Date?
24 | get() = lastUpdateDate
25 |
26 | override suspend fun evict(cleanup: Boolean) {
27 | lastUpdateDate = null
28 | children.forEach { it.evict(cleanup) }
29 | if (cleanup) {
30 | listeners.forEach { it.cleanup() }
31 | }
32 | }
33 |
34 | // Public API
35 | fun createChild(): RefreshControl =
36 | RefreshControl(refreshRate, lastUpdateDate).also { children.add(it) }
37 |
38 | fun addListener(listener: Listener) {
39 | listeners.add(listener)
40 | }
41 |
42 | fun refresh() {
43 | lastUpdateDate = Date()
44 | }
45 |
46 | fun isExpired() = lastUpdateDate?.let { (Date().time - it.time) > refreshRate } ?: true
47 | }
48 |
--------------------------------------------------------------------------------
/features/todos/domain/src/main/java/com/example/todo/todos/domain/di/TodoUseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.domain.di
2 |
3 | import com.example.todo.base.domain.IExceptionHandler
4 | import com.example.todo.todos.data.ITodoApi
5 | import com.example.todo.todos.data.ITodoCache
6 | import com.example.todo.todos.domain.repository.TodoRepository
7 | import com.example.todo.todos.domain.usecase.*
8 | import dagger.Module
9 | import dagger.Provides
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | class TodoUseCaseModule {
14 | @Provides
15 | @Singleton
16 | fun provideTodosRepository(cache: ITodoCache, api: ITodoApi) = TodoRepository(cache, api)
17 |
18 | @Provides
19 | fun provideGetAllTodosUseCase(repository: TodoRepository, handler: IExceptionHandler) =
20 | GetAllTodosUseCase(repository, handler)
21 |
22 | @Provides
23 | fun provideGetTodoByIdUseCase(repository: TodoRepository, handler: IExceptionHandler) =
24 | GetTodoByIdUseCase(repository, handler)
25 |
26 | @Provides
27 | fun provideAddTodoUseCase(repository: TodoRepository, handler: IExceptionHandler) =
28 | AddTodoUseCase(repository, handler)
29 |
30 | @Provides
31 | fun provideDeleteTodoUseCase(repository: TodoRepository, handler: IExceptionHandler) =
32 | DeleteTodoUseCase(repository, handler)
33 |
34 | @Provides
35 | fun provideSetTodoCompletionUseCase(repository: TodoRepository, handler: IExceptionHandler) =
36 | SetTodoCompletionUseCase(repository, handler)
37 | }
38 |
--------------------------------------------------------------------------------
/features/todos/domain/src/main/java/com/example/todo/todos/domain/repository/TodoRepository.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.todos.domain.repository
2 |
3 | import com.example.todo.base.data.ResourceGroup
4 | import com.example.todo.todos.data.ITodoApi
5 | import com.example.todo.todos.data.ITodoCache
6 | import com.example.todo.todos.domain.model.Todo
7 | import kotlinx.coroutines.flow.Flow
8 |
9 | class TodoRepository(
10 | private val localSource: ITodoCache,
11 | private val remoteSource: ITodoApi
12 | ) {
13 | private val resourceGroup = ResourceGroup(
14 | { remoteSource.getAllTodos() },
15 | { localSource.getAllTodos() },
16 | localSource::storeAllTodos,
17 | { id, _ -> remoteSource.getTodoById(id) },
18 | { id, _ -> localSource.getTodoById(id) },
19 | localSource::storeTodo,
20 | localSource::deleteAllTodos
21 | )
22 |
23 | suspend fun getAllTodos(force: Boolean): Flow?> =
24 | resourceGroup.query(Unit, force)
25 |
26 | suspend fun getTodoById(id: Long, force: Boolean): Flow =
27 | resourceGroup.queryByKey(id, Unit, force)
28 |
29 | suspend fun addTodo(todo: Todo): Boolean? =
30 | remoteSource.addTodo(todo).also { resourceGroup.evict() }
31 |
32 | suspend fun deleteTodo(todo: Todo): Boolean? =
33 | remoteSource.deleteTodo(todo).also { resourceGroup.evict() }
34 |
35 | suspend fun updateTodo(todo: Todo, update: Todo): Boolean? =
36 | remoteSource.updateTodo(todo, update).also { resourceGroup.evict() }
37 | }
38 |
--------------------------------------------------------------------------------
/libs/base/data/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | android {
8 | compileSdkVersion 30
9 | buildToolsVersion "30.0.3"
10 |
11 | defaultConfig {
12 | minSdkVersion 23
13 | targetSdkVersion 30
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | consumerProguardFiles "consumer-rules.pro"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 | }
35 |
36 | dependencies {
37 |
38 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
39 | implementation "androidx.core:core-ktx:$core_version"
40 | implementation "androidx.appcompat:appcompat:$compat_version"
41 |
42 | // Room
43 | implementation "androidx.room:room-runtime:$room_version"
44 | kapt "androidx.room:room-compiler:$room_version"
45 | implementation "androidx.room:room-ktx:$room_version"
46 |
47 | implementation project(':baseDomain')
48 |
49 | testImplementation 'junit:junit:4.+'
50 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
51 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
52 | }
53 |
--------------------------------------------------------------------------------
/libs/base/domain/src/main/java/com/example/todo/base/data/Resource.kt:
--------------------------------------------------------------------------------
1 | package com.example.todo.base.data
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import kotlinx.coroutines.flow.flow
5 |
6 | class Resource(
7 | private val remoteFetch: suspend (Input) -> Output?,
8 | private val localFetch: suspend (Input) -> Output?,
9 | private val localStore: suspend (Output) -> Unit,
10 | private val localDelete: suspend () -> Unit,
11 | private val refreshControl: RefreshControl = RefreshControl()
12 | ) : RefreshControl.Listener, ITimeLimitedResource by refreshControl {
13 |
14 | init {
15 | refreshControl.addListener(this)
16 | }
17 |
18 | // Public API
19 | suspend fun query(args: Input, force: Boolean = false): Flow