Commit 432ba666 authored by Mark Murphy's avatar Mark Murphy

add new tutorials

parent d66a1af8
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs.kotlin'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.commonsware.todo"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
androidExtensions {
experimental = true
}
dataBinding {
enabled = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.fragment:fragment-ktx:1.0.0'
implementation "androidx.lifecycle:lifecycle-livedata:2.0.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "org.koin:koin-core:$koin_version"
implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-androidx-viewmodel:$koin_version"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-coroutines:$room_version"
implementation "com.github.jknack:handlebars:4.1.2"
kapt "androidx.room:room-compiler:$room_version"
testImplementation 'junit:junit:4.12'
testImplementation "androidx.arch.core:core-testing:2.0.0"
testImplementation "org.amshove.kluent:kluent-android:1.49"
testImplementation "org.mockito:mockito-inline:2.21.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
testImplementation 'com.jraska.livedata:testing-ktx:1.1.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.1.1'
androidTestImplementation "androidx.arch.core:core-testing:2.0.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
package com.commonsware.todo.ui.roster
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.hasChildCount
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.commonsware.todo.R
import com.commonsware.todo.repo.ToDoDatabase
import com.commonsware.todo.repo.ToDoModel
import com.commonsware.todo.repo.ToDoRepository
import com.commonsware.todo.ui.MainActivity
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.dsl.module.module
import org.koin.standalone.StandAloneContext.loadKoinModules
@RunWith(AndroidJUnit4::class)
class RosterListFragmentTest {
private lateinit var repo: ToDoRepository
private val items = listOf(
ToDoModel("this is a test"),
ToDoModel("this is another test"),
ToDoModel("this is... wait for it... yet another test")
)
@Before
fun setUp() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val db = ToDoDatabase.newTestInstance(context)
repo = ToDoRepository(db.todoStore())
loadKoinModules(module {
single(override = true) { repo }
})
runBlocking { items.forEach { repo.save(it) } }
}
@Test
fun testListContents() {
ActivityScenario.launch(MainActivity::class.java)
onView(withId(R.id.items)).check(matches(hasChildCount(3)))
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.commonsware.todo"
xmlns:android="http://schemas.android.com/apk/res/android">
<supports-screens
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:xlargeScreens="true" />
<application
android:name=".ToDoApp"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".ui.AboutActivity"></activity>
<activity android:name=".ui.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
<h1>About This App</h1>
<p>This app is cool!</p>
<p>No, really &mdash; this app is awesome!</p>
<div>
.
<br/>
.
<br/>
.
<br/>
.
</div>
<p>OK, this app isn't all that much. But, hey, it's mine!</p>
\ No newline at end of file
package com.commonsware.todo
import android.app.Application
import android.text.format.DateUtils
import com.commonsware.todo.repo.ToDoDatabase
import com.commonsware.todo.repo.ToDoRepository
import com.commonsware.todo.report.RosterReport
import com.commonsware.todo.ui.SingleModelMotor
import com.commonsware.todo.ui.roster.RosterMotor
import com.github.jknack.handlebars.Handlebars
import com.github.jknack.handlebars.Helper
import org.koin.android.ext.android.startKoin
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.ext.koin.viewModel
import org.koin.dsl.module.module
import java.util.*
class ToDoApp : Application() {
private val koinModule = module {
single { ToDoDatabase.newInstance(androidContext()) }
single {
val db: ToDoDatabase = get()
ToDoRepository(db.todoStore())
}
single {
Handlebars().apply {
registerHelper("dateFormat", Helper<Calendar> { value, _ ->
DateUtils
.getRelativeDateTimeString(
androidContext(), value.timeInMillis,
DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0
)
})
}
}
single { RosterReport(androidContext(), get()) }
viewModel { RosterMotor(get(), get()) }
viewModel { (modelId: String) -> SingleModelMotor(get(), modelId) }
}
override fun onCreate() {
super.onCreate()
startKoin(this, listOf(koinModule))
}
}
\ No newline at end of file
package com.commonsware.todo.repo
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
private const val DB_NAME = "stuff.db"
@Database(entities = [ToDoEntity::class], version = 1)
@TypeConverters(TypeTransmogrifier::class)
abstract class ToDoDatabase : RoomDatabase() {
abstract fun todoStore(): ToDoEntity.Store
companion object {
fun newInstance(context: Context) =
Room.databaseBuilder(context, ToDoDatabase::class.java, DB_NAME).build()
fun newTestInstance(context: Context) =
Room.inMemoryDatabaseBuilder(context, ToDoDatabase::class.java).build()
}
}
package com.commonsware.todo.repo
import androidx.lifecycle.LiveData
import androidx.room.*
import java.util.*
@Entity(tableName = "todos", indices = [Index(value = ["id"])])
data class ToDoEntity(
val description: String,
@field:PrimaryKey
val id: String = UUID.randomUUID().toString(),
val notes: String = "",
val createdOn: Calendar = Calendar.getInstance(),
val isCompleted: Boolean = false
) {
constructor(model: ToDoModel): this(
id = model.id,
description = model.description,
isCompleted = model.isCompleted,
notes = model.notes,
createdOn = model.createdOn
)
fun toModel(): ToDoModel {
return ToDoModel(
id = id,
description = description,
isCompleted = isCompleted,
notes = notes,
createdOn = createdOn
)
}
@Dao
interface Store {
@Query("SELECT * FROM todos ORDER BY description")
fun all(): LiveData<List<ToDoEntity>>
@Query("SELECT * FROM todos WHERE isCompleted = :isCompleted ORDER BY description")
fun filtered(isCompleted: Boolean): LiveData<List<ToDoEntity>>
@Query("SELECT * FROM todos WHERE id = :modelId ORDER BY description")
fun find(modelId: String): LiveData<ToDoEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(vararg entities: ToDoEntity)
@Delete
suspend fun delete(vararg entities: ToDoEntity)
}
}
\ No newline at end of file
package com.commonsware.todo.repo
import java.util.*
data class ToDoModel(
val description: String,
val id: String = UUID.randomUUID().toString(),
val isCompleted: Boolean = false,
val notes: String = "",
val createdOn: Calendar = Calendar.getInstance()
) {
}
\ No newline at end of file
package com.commonsware.todo.repo
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
enum class FilterMode { ALL, OUTSTANDING, COMPLETED }
class ToDoRepository(private val store: ToDoEntity.Store) {
fun items(filterMode: FilterMode = FilterMode.ALL): LiveData<List<ToDoModel>> =
Transformations.map(filteredEntities(filterMode)) { all -> all.map { it.toModel() } }
fun find(id: String): LiveData<ToDoModel> =
Transformations.map(store.find(id)) { it.toModel() }
suspend fun save(model: ToDoModel) {
store.save(ToDoEntity(model))
}
suspend fun delete(model: ToDoModel) {
store.delete(ToDoEntity(model))
}
private fun filteredEntities(filterMode: FilterMode) = when (filterMode) {
FilterMode.ALL -> store.all()
FilterMode.OUTSTANDING -> store.filtered(isCompleted = false)
FilterMode.COMPLETED -> store.filtered(isCompleted = true)
}
}
\ No newline at end of file
package com.commonsware.todo.repo
import androidx.room.TypeConverter
import java.util.*
class TypeTransmogrifier {
@TypeConverter
fun fromCalendar(date: Calendar?): Long? = date?.timeInMillis
@TypeConverter
fun toCalendar(millisSinceEpoch: Long?): Calendar? = millisSinceEpoch?.let {
Calendar.getInstance().apply { timeInMillis = millisSinceEpoch }
}
}
\ No newline at end of file
package com.commonsware.todo.report
import android.content.Context
import android.net.Uri
import com.commonsware.todo.R
import com.commonsware.todo.repo.ToDoModel
import com.github.jknack.handlebars.Handlebars
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.OutputStreamWriter
class RosterReport(private val context: Context, engine: Handlebars) {
private val template =
engine.compileInline(context.getString(R.string.report_template))
suspend fun generate(content: List<ToDoModel>, doc: Uri) {
withContext(Dispatchers.IO) {
OutputStreamWriter(context.contentResolver.openOutputStream(doc)).use { osw ->
osw.write(template.apply(content))
osw.flush()
}
}
}
}
\ No newline at end of file
package com.commonsware.todo.ui
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.commonsware.todo.R
import kotlinx.android.synthetic.main.activity_about.*
class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_about)
toolbar.title = getString(R.string.app_name)
about.loadUrl("file:///android_asset/about.html")
}
}
package com.commonsware.todo.ui
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import com.commonsware.todo.R
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
findNavController(R.id.nav_host).let { nav ->
appBarConfiguration = AppBarConfiguration(nav.graph)
setupActionBarWithNavController(nav, appBarConfiguration)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.actions, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.about) {
startActivity(Intent(this, AboutActivity::class.java))
return true
}
return super.onOptionsItemSelected(item)
}
override fun onSupportNavigateUp() =
navigateUp(findNavController(R.id.nav_host), appBarConfiguration)
}
package com.commonsware.todo.ui
import androidx.lifecycle.*
import com.commonsware.todo.repo.ToDoModel
import com.commonsware.todo.repo.ToDoRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
class SingleModelViewState(
val item: ToDoModel? = null
)
class SingleModelMotor(
private val repo: ToDoRepository,
modelId: String?,
private val uiContext: CoroutineContext = Dispatchers.Main
) : ViewModel() {
val states: LiveData<SingleModelViewState> =
modelId?.let { Transformations.map(repo.find(modelId)) { SingleModelViewState(it) } }
?: MutableLiveData<SingleModelViewState>().apply { value = SingleModelViewState(null) }
fun save(model: ToDoModel) {
viewModelScope.launch(uiContext) {
repo.save(model)
}
}
fun delete(model: ToDoModel) {
viewModelScope.launch(uiContext) {
repo.delete(model)
}
}
}
\ No newline at end of file
package com.commonsware.todo.ui.display
import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.commonsware.todo.R
import com.commonsware.todo.databinding.TodoDisplayBinding
import com.commonsware.todo.ui.SingleModelMotor