Final Results
The top-level build.gradle
file should look a bit like:
buildscript {
ext.nav_version = '2.3.5'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
ext {
koin_version = "3.1.2"
moshi_version = "1.12.0"
room_version = "2.3.0"
}
app/build.gradle
should contain:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'androidx.navigation.safeargs.kotlin'
id 'kotlin-kapt'
}
android {
compileSdk 31
defaultConfig {
applicationId "com.commonsware.todo"
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
packagingOptions {
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.1'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.preference:preference-ktx:1.1.1"
implementation 'com.google.android.material:material:1.4.0'
implementation "io.insert-koin:koin-android:$koin_version"
implementation "com.github.jknack:handlebars:4.1.2"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation "com.squareup.okhttp3:okhttp:4.9.1"
implementation "com.squareup.moshi:moshi:$moshi_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
kapt "androidx.room:room-compiler:$room_version"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
testImplementation 'junit:junit:4.13.2'
testImplementation "org.mockito:mockito-inline:3.12.1"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.1'
}
The manifest overall should resemble:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.commonsware.todo">
<uses-permission android:name="android.permission.INTERNET" />
<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/Theme.ToDo">
<activity
android:name=".ui.AboutActivity"
android:exported="true" />
<activity
android:name=".ui.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>
Our new ToDoServerItem.kt
should contain:
package com.commonsware.todo.repo
import android.annotation.SuppressLint
import com.squareup.moshi.FromJson
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.ToJson
import java.time.Instant
import java.time.format.DateTimeFormatter
@JsonClass(generateAdapter = true)
data class ToDoServerItem(
val description: String,
val id: String,
val completed: Boolean,
val notes: String,
@Json(name = "created_on") val createdOn: Instant
) {
fun toEntity(): ToDoEntity {
return ToDoEntity(
id = id,
description = description,
isCompleted = completed,
notes = notes,
createdOn = createdOn
)
}
}
private val FORMATTER = DateTimeFormatter.ISO_INSTANT
class MoshiInstantAdapter {
@ToJson
fun toJson(date: Instant) = FORMATTER.format(date)
@FromJson
fun fromJson(dateString: String): Instant =
FORMATTER.parse(dateString, Instant::from)
}
ToDoRepository
should resemble:
package com.commonsware.todo.repo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
enum class FilterMode { ALL, OUTSTANDING, COMPLETED }
class ToDoRepository(
private val store: ToDoEntity.Store,
private val appScope: CoroutineScope,
private val remoteDataSource: ToDoRemoteDataSource
) {
fun items(filterMode: FilterMode = FilterMode.ALL): Flow<List<ToDoModel>> =
filteredEntities(filterMode).map { all -> all.map { it.toModel() } }
private fun filteredEntities(filterMode: FilterMode) = when (filterMode) {
FilterMode.ALL -> store.all()
FilterMode.OUTSTANDING -> store.filtered(isCompleted = false)
FilterMode.COMPLETED -> store.filtered(isCompleted = true)
}
fun find(id: String?): Flow<ToDoModel?> = store.find(id).map { it?.toModel() }
suspend fun save(model: ToDoModel) {
withContext(appScope.coroutineContext) {
store.save(ToDoEntity(model))
}
}
suspend fun delete(model: ToDoModel) {
withContext(appScope.coroutineContext) {
store.delete(ToDoEntity(model))
}
}
suspend fun importItems(url: String) {
withContext(appScope.coroutineContext) {
store.importItems(remoteDataSource.load(url).map { it.toEntity() })
}
}
}
ToDoEntity
should contain:
package com.commonsware.todo.repo
import androidx.room.*
import kotlinx.coroutines.flow.Flow
import java.time.Instant
import java.util.*
@Entity(tableName = "todos", indices = [Index(value = ["id"])])
data class ToDoEntity(
val description: String,
@PrimaryKey
val id: String = UUID.randomUUID().toString(),
val notes: String = "",
val createdOn: Instant = Instant.now(),
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(): Flow<List<ToDoEntity>>
@Query("SELECT * FROM todos WHERE isCompleted = :isCompleted ORDER BY description")
fun filtered(isCompleted: Boolean): Flow<List<ToDoEntity>>
@Query("SELECT * FROM todos WHERE id = :modelId")
fun find(modelId: String?): Flow<ToDoEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun save(vararg entities: ToDoEntity)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun importItems(entities: List<ToDoEntity>)
@Delete
suspend fun delete(vararg entities: ToDoEntity)
}
}
The repaired RosterListFragmentTest
should look like:
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.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.ToDoRemoteDataSource
import com.commonsware.todo.repo.ToDoRepository
import com.commonsware.todo.ui.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import org.junit.Before
import org.junit.Test
import org.koin.core.context.loadKoinModules
import org.koin.dsl.module
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)
val appScope = CoroutineScope(SupervisorJob())
repo = ToDoRepository(
db.todoStore(),
appScope,
ToDoRemoteDataSource(OkHttpClient())
)
loadKoinModules(module {
single { repo }
})
runBlocking { items.forEach { repo.save(it) } }
}
@Test
fun testListContents() {
ActivityScenario.launch(MainActivity::class.java)
onView(withId(R.id.items)).check(matches(hasChildCount(3)))
}
}
And the fixed ToDoRepositoryTest
should contain:
package com.commonsware.todo.repo
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runBlockingTest
import okhttp3.OkHttpClient
import org.hamcrest.Matchers.empty
import org.hamcrest.Matchers.equalTo
import org.hamcrest.collection.IsIterableContainingInOrder.contains
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ToDoRepositoryTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val db = ToDoDatabase.newTestInstance(context)
private val remoteDataSource = ToDoRemoteDataSource(OkHttpClient())
@Test
fun canAddItems() = runBlockingTest {
val underTest = ToDoRepository(db.todoStore(), this, remoteDataSource)
val results = mutableListOf<List<ToDoModel>>()
val itemsJob = launch {
underTest.items().collect { results.add(it) }
}
assertThat(results.size, equalTo(1))
assertThat(results[0], empty())
val testModel = ToDoModel("test model")
underTest.save(testModel)
assertThat(results.size, equalTo(2))
assertThat(results[1], contains(testModel))
assertThat(underTest.find(testModel.id).first(), equalTo(testModel))
itemsJob.cancel()
}
@Test
fun canModifyItems() = runBlockingTest {
val underTest = ToDoRepository(db.todoStore(), this, remoteDataSource)
val testModel = ToDoModel("test model")
val replacement = testModel.copy(notes = "This is the replacement")
val results = mutableListOf<List<ToDoModel>>()
val itemsJob = launch {
underTest.items().collect { results.add(it) }
}
assertThat(results[0], empty())
underTest.save(testModel)
assertThat(results[1], contains(testModel))
underTest.save(replacement)
assertThat(results[2], contains(replacement))
itemsJob.cancel()
}
@Test
fun canRemoveItems() = runBlockingTest {
val underTest = ToDoRepository(db.todoStore(), this, remoteDataSource)
val testModel = ToDoModel("test model")
val results = mutableListOf<List<ToDoModel>>()
val itemsJob = launch {
underTest.items().collect { results.add(it) }
}
assertThat(results[0], empty())
underTest.save(testModel)
assertThat(results[1], contains(testModel))
underTest.delete(testModel)
assertThat(results[2], empty())
itemsJob.cancel()
}
}
The new PrefsRepository
should be:
package com.commonsware.todo.repo
import android.content.Context
import androidx.preference.PreferenceManager
import com.commonsware.todo.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class PrefsRepository(context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val webServiceUrlKey = context.getString(R.string.web_service_url_key)
private val defaultWebServiceUrl =
context.getString(R.string.web_service_url_default)
suspend fun loadWebServiceUrl(): String = withContext(Dispatchers.IO) {
prefs.getString(webServiceUrlKey, defaultWebServiceUrl) ?: defaultWebServiceUrl
}
}
ToDoApp
should look like:
package com.commonsware.todo
import android.app.Application
import android.text.format.DateUtils
import com.commonsware.todo.repo.PrefsRepository
import com.commonsware.todo.repo.ToDoDatabase
import com.commonsware.todo.repo.ToDoRemoteDataSource
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.qualifier.named
import org.koin.dsl.module
import java.time.Instant
class ToDoApp : Application() {
private val koinModule = module {
single(named("appScope")) { CoroutineScope(SupervisorJob()) }
single { ToDoDatabase.newInstance(androidContext()) }
single {
ToDoRepository(
get<ToDoDatabase>().todoStore(),
get(named("appScope")),
get()
)
}
single {
Handlebars().apply {
registerHelper("dateFormat", Helper<Instant> { value, _ ->
DateUtils.getRelativeDateTimeString(
androidContext(),
value.toEpochMilli(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS, 0
)
})
}
}
single { RosterReport(androidContext(), get(), get(named("appScope"))) }
single { OkHttpClient.Builder().build() }
single { ToDoRemoteDataSource(get()) }
single { PrefsRepository(androidContext()) }
viewModel {
RosterMotor(
get(),
get(),
androidApplication(),
get(named("appScope")),
get()
)
}
viewModel { (modelId: String) -> SingleModelMotor(get(), modelId) }
}
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@ToDoApp)
modules(koinModule)
}
}
}
RosterMotor
, should contain:
package com.commonsware.todo.ui.roster
import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.commonsware.todo.BuildConfig
import com.commonsware.todo.repo.FilterMode
import com.commonsware.todo.repo.PrefsRepository
import com.commonsware.todo.repo.ToDoModel
import com.commonsware.todo.repo.ToDoRepository
import com.commonsware.todo.report.RosterReport
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.io.File
private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".provider"
data class RosterViewState(
val items: List<ToDoModel> = listOf(),
val isLoaded: Boolean = false,
val filterMode: FilterMode = FilterMode.ALL
)
sealed class Nav {
data class ViewReport(val doc: Uri) : Nav()
data class ShareReport(val doc: Uri) : Nav()
}
class RosterMotor(
private val repo: ToDoRepository,
private val report: RosterReport,
private val context: Application,
private val appScope: CoroutineScope,
private val prefs: PrefsRepository
) : ViewModel() {
private val _states = MutableStateFlow(RosterViewState())
val states = _states.asStateFlow()
private val _navEvents = MutableSharedFlow<Nav>()
val navEvents = _navEvents.asSharedFlow()
private var job: Job? = null
init {
load(FilterMode.ALL)
}
fun load(filterMode: FilterMode) {
job?.cancel()
job = viewModelScope.launch {
repo.items(filterMode).collect {
_states.emit(RosterViewState(it, true, filterMode))
}
}
}
fun save(model: ToDoModel) {
viewModelScope.launch {
repo.save(model)
}
}
fun saveReport(doc: Uri) {
viewModelScope.launch {
report.generate(_states.value.items, doc)
_navEvents.emit(Nav.ViewReport(doc))
}
}
fun shareReport() {
viewModelScope.launch {
saveForSharing()
}
}
fun importItems() {
viewModelScope.launch {
repo.importItems(prefs.loadWebServiceUrl())
}
}
private suspend fun saveForSharing() {
withContext(Dispatchers.IO + appScope.coroutineContext) {
val shared = File(context.cacheDir, "shared").also { it.mkdirs() }
val reportFile = File(shared, "report.html")
val doc = FileProvider.getUriForFile(context, AUTHORITY, reportFile)
_states.value.let { report.generate(it.items, doc) }
_navEvents.emit(Nav.ShareReport(doc))
}
}
}
The actions_roster
menu resource should resemble:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/filter"
android:icon="@drawable/ic_filter"
android:title="@string/menu_filter"
app:showAsAction="ifRoom|withText">
<menu>
<group
android:id="@+id/filter_group"
android:checkableBehavior="single" >
<item
android:id="@+id/all"
android:checked="true"
android:title="@string/menu_filter_all" />
<item
android:id="@+id/completed"
android:title="@string/menu_filter_completed" />
<item
android:id="@+id/outstanding"
android:title="@string/menu_filter_outstanding" />
</group>
</menu>
</item>
<item
android:id="@+id/add"
android:icon="@drawable/ic_add"
android:title="@string/menu_add"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/save"
android:icon="@drawable/ic_save"
android:title="@string/menu_save"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/share"
android:icon="@drawable/ic_share"
android:title="@string/menu_share"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/importItems"
android:icon="@drawable/ic_download"
android:title="@string/menu_import"
app:showAsAction="never" />
</menu>
The strings
resource should look like:
<resources>
<string name="app_name">ToDo</string>
<string name="msg_empty">Click the + icon to add a todo item!</string>
<string name="msg_empty_filtered">Click the + icon to add a todo item, or change your filter to show other items</string>
<string name="menu_about">About</string>
<string name="is_completed">Item is completed</string>
<string name="created_on">Created on:</string>
<string name="menu_edit">Edit</string>
<string name="desc">Description</string>
<string name="notes">Notes</string>
<string name="menu_save">Save</string>
<string name="menu_add">Add</string>
<string name="menu_delete">Delete</string>
<string name="menu_filter">Filter</string>
<string name="menu_filter_all">All</string>
<string name="menu_filter_completed">Completed</string>
<string name="menu_filter_outstanding">Outstanding</string>
<string name="oops">Sorry! Something went wrong!</string>
<string name="report_template"><![CDATA[<h1>To-Do Items</h1>
<h2></h2>
<p><b>COMPLETED</b> — Created on: </p>
<p></p>
]]></string>
<string name="menu_share">Share</string>
<string name="pref_url_title">Web service URL</string>
<string name="web_service_url_key">webServiceUrl</string>
<string name="web_service_url_default">https://commonsware.com/AndExplore/2.0/items.json</string>
<string name="settings">Settings</string>
<string name="menu_import">Import</string>
</resources>
And RosterListFragment
should resemble:
package com.commonsware.todo.ui.roster
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.commonsware.todo.R
import com.commonsware.todo.databinding.TodoRosterBinding
import com.commonsware.todo.repo.FilterMode
import com.commonsware.todo.repo.ToDoModel
import kotlinx.coroutines.flow.collect
import org.koin.androidx.viewmodel.ext.android.viewModel
private const val TAG = "ToDo"
class RosterListFragment : Fragment() {
private val motor: RosterMotor by viewModel()
private val menuMap = mutableMapOf<FilterMode, MenuItem>()
private var binding: TodoRosterBinding? = null
private val createDoc =
registerForActivityResult(ActivityResultContracts.CreateDocument()) {
motor.saveReport(it)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = TodoRosterBinding.inflate(inflater, container, false)
.also { binding = it }
.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = RosterAdapter(
layoutInflater,
onCheckboxToggle = { motor.save(it.copy(isCompleted = !it.isCompleted)) },
onRowClick = ::display
)
binding?.items?.apply {
setAdapter(adapter)
layoutManager = LinearLayoutManager(context)
addItemDecoration(
DividerItemDecoration(
activity,
DividerItemDecoration.VERTICAL
)
)
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
motor.states.collect { state ->
adapter.submitList(state.items)
binding?.apply {
loading.visibility = if (state.isLoaded) View.GONE else View.VISIBLE
when {
state.items.isEmpty() && state.filterMode == FilterMode.ALL -> {
empty.visibility = View.VISIBLE
empty.setText(R.string.msg_empty)
}
state.items.isEmpty() -> {
empty.visibility = View.VISIBLE
empty.setText(R.string.msg_empty_filtered)
}
else -> empty.visibility = View.GONE
}
}
menuMap[state.filterMode]?.isChecked = true
}
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
motor.navEvents.collect { nav ->
when (nav) {
is Nav.ViewReport -> viewReport(nav.doc)
is Nav.ShareReport -> shareReport(nav.doc)
}
}
}
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.actions_roster, menu)
menuMap.apply {
put(FilterMode.ALL, menu.findItem(R.id.all))
put(FilterMode.COMPLETED, menu.findItem(R.id.completed))
put(FilterMode.OUTSTANDING, menu.findItem(R.id.outstanding))
}
menuMap[motor.states.value.filterMode]?.isChecked = true
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.add -> {
add()
return true
}
R.id.all -> {
item.isChecked = true
motor.load(FilterMode.ALL)
return true
}
R.id.completed -> {
item.isChecked = true
motor.load(FilterMode.COMPLETED)
return true
}
R.id.outstanding -> {
item.isChecked = true
motor.load(FilterMode.OUTSTANDING)
return true
}
R.id.save -> {
saveReport()
return true
}
R.id.share -> {
motor.shareReport()
return true
}
R.id.importItems -> {
motor.importItems()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun display(model: ToDoModel) {
findNavController()
.navigate(RosterListFragmentDirections.displayModel(model.id))
}
private fun add() {
findNavController().navigate(RosterListFragmentDirections.createModel(null))
}
private fun saveReport() {
createDoc.launch("report.html")
}
private fun viewReport(uri: Uri) {
safeStartActivity(
Intent(Intent.ACTION_VIEW, uri)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
)
}
private fun shareReport(doc: Uri) {
safeStartActivity(
Intent(Intent.ACTION_SEND)
.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType("text/html")
.putExtra(Intent.EXTRA_STREAM, doc)
)
}
private fun safeStartActivity(intent: Intent) {
try {
startActivity(intent)
} catch (t: Throwable) {
Log.e(TAG, "Exception starting $intent", t)
Toast.makeText(requireActivity(), R.string.oops, Toast.LENGTH_LONG).show()
}
}
}
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.