refactoring hltb example to use navigation

This commit is contained in:
Wouter Groeneveld 2021-08-17 12:17:43 +02:00
parent 33f9786d39
commit ec81c2a24f
31 changed files with 447 additions and 291 deletions

View File

@ -3,7 +3,7 @@ package be.kuleuven.howlongtobeat.model.room
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import be.kuleuven.howlongtobeat.model.Todo
import be.kuleuven.howlongtobeat.model.Game
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
@ -11,15 +11,15 @@ import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TodoPersistenceTests {
class GamePersistenceTests {
private lateinit var db: TodoDatabase
private lateinit var dao: TodoDao
private lateinit var db: GameDatabase
private lateinit var dao: GameDao
@Before
fun setUp() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
db = Room.inMemoryDatabaseBuilder(appContext, TodoDatabase::class.java)
db = Room.inMemoryDatabaseBuilder(appContext, GameDatabase::class.java)
.setQueryCallback(LogQueryCallBack(), CurrentThreadExecutor())
.build()
db.clearAllTables()
@ -33,7 +33,7 @@ class TodoPersistenceTests {
@Test
fun todoItemCanBePersisted() {
val item = Todo("brush my little pony", false)
val item = Game("brush my little pony", false)
dao.insert(arrayListOf(item))
val refreshedItem = dao.query().single()
@ -46,7 +46,7 @@ class TodoPersistenceTests {
@Test
fun updateUpdatesTodoPropertiesInDb() {
var todo = Todo("git good at Hollow Knight", false)
var todo = Game("git good at Hollow Knight", false)
dao.insert(arrayListOf(todo))
todo = dao.query().single() // refresh to get the ID, otherwise update() will update where ID = 0
@ -61,7 +61,7 @@ class TodoPersistenceTests {
}
}
private fun finallyFinishHollowKnight(item: Todo) {
private fun finallyFinishHollowKnight(item: Game) {
println("Congrats! On to Demon Souls?")
item.check()
}

View File

@ -3,6 +3,7 @@
package="be.kuleuven.howlongtobeat">
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:allowBackup="true"

View File

@ -6,21 +6,18 @@ import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import be.kuleuven.howlongtobeat.model.Todo
import be.kuleuven.howlongtobeat.model.Game
class TodoAdapter(val items: List<Todo>) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
class GameListAdapter(val items: List<Game>) : RecyclerView.Adapter<GameListAdapter.GameListViewHolder>() {
inner class TodoViewHolder(currentItemView: View) : RecyclerView.ViewHolder(currentItemView)
inner class GameListViewHolder(currentItemView: View) : RecyclerView.ViewHolder(currentItemView)
// this creates the needed ViewHolder class that links our layout XML to our viewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
// don't forget to set attachToRoot to false, otherwise it will crash!
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_todo, parent, false)
return TodoViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameListViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_game, parent, false)
return GameListViewHolder(view)
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
// bind the data to our items: set the todo text view text and checked state accordingly
override fun onBindViewHolder(holder: GameListViewHolder, position: Int) {
val currentTodoItem = items[position]
holder.itemView.apply {
val checkBoxTodo = findViewById<CheckBox>(R.id.chkTodoDone)

View File

@ -0,0 +1,75 @@
package be.kuleuven.howlongtobeat
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import be.kuleuven.howlongtobeat.databinding.FragmentGamelistBinding
import be.kuleuven.howlongtobeat.model.Game
import be.kuleuven.howlongtobeat.model.GameRepository
import be.kuleuven.howlongtobeat.model.room.GameRepositoryRoomImpl
class GameListFragment : Fragment(R.layout.fragment_gamelist) {
private val gameList = arrayListOf<Game>()
private lateinit var binding: FragmentGamelistBinding
private lateinit var main: MainActivity
private lateinit var adapter: GameListAdapter
private lateinit var gameRepository: GameRepository
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentGamelistBinding.inflate(layoutInflater)
main = activity as MainActivity
gameRepository = GameRepositoryRoomImpl(main.applicationContext)
loadGames()
adapter = GameListAdapter(gameList)
binding.rvGameList.adapter = adapter
binding.rvGameList.layoutManager = LinearLayoutManager(this.context)
binding.btnAddTodo.setOnClickListener(this::addNewTotoItem)
return binding.root
}
private fun loadGames() {
gameList.addAll(gameRepository.load())
if(!gameList.any()) {
gameList.add(Game.NONE_YET)
}
}
private fun addNewTotoItem(it: View) {
findNavController().navigate(R.id.action_gameListFragment_to_loadingFragment)
}
/*
fun onHltbGamesRetrieved(games: List<HowLongToBeatResult>) {
gameList.clear()
gameList.addAll(games.map { Game("${it.title} (${it.howlong})", false) })
adapter.notifyDataSetChanged()
}
fun clearAllItems() {
gameList.clear()
adapter.notifyDataSetChanged()
}
fun clearLatestItem() {
if(gameList.size >= 1) {
gameList.removeAt(gameList.size - 1)
adapter.notifyItemRemoved(gameList.size - 1)
}
}
*/
}

View File

@ -0,0 +1,28 @@
package be.kuleuven.howlongtobeat
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
class HltbResultsAdapter(private val items: List<HowLongToBeatResult>) : RecyclerView.Adapter<HltbResultsAdapter.HltbResultsViewHolder>() {
inner class HltbResultsViewHolder(currentItemView: View) : RecyclerView.ViewHolder(currentItemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HltbResultsViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_hltbresult, parent, false)
return HltbResultsViewHolder(view)
}
override fun onBindViewHolder(holder: HltbResultsViewHolder, position: Int) {
val currentResult = items[position]
holder.itemView.apply {
val txtHltbItemResult = findViewById<TextView>(R.id.txtHltbItemResult)
txtHltbItemResult.text = currentResult.title
}
}
override fun getItemCount(): Int = items.size
}

View File

@ -0,0 +1,31 @@
package be.kuleuven.howlongtobeat
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import be.kuleuven.howlongtobeat.databinding.FragmentHltbresultsBinding
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
class HltbResultsFragment : Fragment(R.layout.fragment_hltbresults) {
private lateinit var binding: FragmentHltbresultsBinding
private lateinit var adapter: HltbResultsAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentHltbresultsBinding.inflate(layoutInflater)
val resultFromLoadingFragment = arguments?.getSerializable(HowLongToBeatResult.RESULT) as List<HowLongToBeatResult>
adapter = HltbResultsAdapter(resultFromLoadingFragment)
binding.rvHltbResult.adapter = adapter
binding.rvHltbResult.layoutManager = LinearLayoutManager(this.context)
return binding.root
}
}

View File

@ -1,5 +1,127 @@
package be.kuleuven.howlongtobeat
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import be.kuleuven.howlongtobeat.cartridges.Cartridge
import be.kuleuven.howlongtobeat.cartridges.CartridgesRepository
import be.kuleuven.howlongtobeat.databinding.FragmentLoadingBinding
import be.kuleuven.howlongtobeat.google.GoogleVisionClient
import be.kuleuven.howlongtobeat.hltb.HLTBClient
import be.kuleuven.howlongtobeat.hltb.HowLongToBeatResult
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
class LoadingFragment : Fragment(R.layout.fragment_loading)
class LoadingFragment : Fragment(R.layout.fragment_loading) {
private lateinit var hltbClient: HLTBClient
private lateinit var cartRepo: CartridgesRepository
private lateinit var visionClient: GoogleVisionClient
private lateinit var cameraPermissionActivityResult: ActivityResultLauncher<String>
private lateinit var cameraActivityResult: ActivityResultLauncher<Void>
private lateinit var main: MainActivity
private lateinit var binding: FragmentLoadingBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentLoadingBinding.inflate(layoutInflater)
main = activity as MainActivity
cartRepo = CartridgesRepository.fromAsset(main.applicationContext)
visionClient = GoogleVisionClient()
hltbClient = HLTBClient(main.applicationContext)
cameraActivityResult = registerForActivityResult(ActivityResultContracts.TakePicturePreview(), this::cameraSnapTaken)
cameraPermissionActivityResult = registerForActivityResult(ActivityResultContracts.RequestPermission(), this::cameraPermissionAcceptedOrDenied)
tryToMakeCameraSnap()
return binding.root
}
private fun cameraSnapTaken(pic: Bitmap) {
MainScope().launch{
findGameBasedOnCameraSnap(pic)
}
}
private suspend fun findGameBasedOnCameraSnap(pic: Bitmap) {
progress("Unleashing Google Vision on the pic...")
// TODO remove in future
val dummypic = BitmapFactory.decodeResource(resources, R.drawable.supermarioland2)
val cartCode = visionClient.findCartCodeViaGoogleVision(dummypic)
if (cartCode == null) {
errorInProgress("Unable to find a code in your pic. Retry?")
return
}
progress("Found cart code $cartCode, looking in DB...")
val foundCart = cartRepo.find(cartCode)
if (foundCart == Cartridge.UNKNOWN_CART) {
errorInProgress("$cartCode is an unknown game cartridge. Retry?")
return
}
progress("Valid cart code $cartCode, looking in HLTB...")
hltbClient.find(foundCart.title) {
Snackbar.make(requireView(), "Found ${it.size} game(s) for cart $cartCode", Snackbar.LENGTH_LONG).show()
// TODO wat als geen hltb results gevonden?
val bundle = bundleOf(HowLongToBeatResult.RESULT to it, HowLongToBeatResult.CODE to cartCode)
findNavController().navigate(R.id.action_loadingFragment_to_hltbResultsFragment, bundle)
}
}
private fun cameraPermissionAcceptedOrDenied(succeeded: Boolean) {
if(succeeded) {
makeCameraSnap()
} else {
progress("Camera permission required!")
}
}
private fun tryToMakeCameraSnap() {
progress("Making snapshot with camera...")
if(ContextCompat.checkSelfPermission(main, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
cameraPermissionActivityResult.launch(Manifest.permission.CAMERA)
}
makeCameraSnap()
}
private fun makeCameraSnap() {
cameraActivityResult.launch(null)
}
private fun progress(msg: String) {
if(!binding.indeterminateBar.isAnimating) {
binding.indeterminateBar.animate()
}
binding.txtLoading.text = msg
}
private fun errorInProgress(msg: String) {
progress(msg)
binding.indeterminateBar.clearAnimation()
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show()
// todo show retry button or something?
}
}

View File

@ -1,77 +1,33 @@
package be.kuleuven.howlongtobeat
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import be.kuleuven.howlongtobeat.cartridges.CartridgesRepository
import be.kuleuven.howlongtobeat.databinding.ActivityMainBinding
import be.kuleuven.howlongtobeat.google.GoogleVisionClient
import be.kuleuven.howlongtobeat.hltb.HLTBClient
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var menuBarToggle: ActionBarDrawerToggle
private var todoFragment = TodoFragment()
private var loadingFragment = LoadingFragment()
private lateinit var hltbClient: HLTBClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setCurrentFragment(loadingFragment)
setupMenuDrawer()
hltbClient = HLTBClient(applicationContext) {
todoFragment.onHltbGamesRetrieved(it)
}
val cartRepo = CartridgesRepository.fromAsset(applicationContext)
val sml2SampleData = BitmapFactory.decodeResource(resources, R.drawable.supermarioland2)
MainScope().launch{
val cartCode = GoogleVisionClient().findCartCodeViaGoogleVision(sml2SampleData)
val foundCart = cartRepo.find(cartCode)
withContext(Dispatchers.Main) {
Snackbar.make(binding.root, "Found cart: ${foundCart.title}!", Snackbar.LENGTH_LONG).show()
}
setCurrentFragment(todoFragment)
hltbClient.triggerFind(foundCart.title)
}
setContentView(binding.root)
}
private fun setCurrentFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction().apply {
replace(R.id.frmTodoContainer, fragment)
commit()
}
}
private fun setupMenuDrawer() {
menuBarToggle = ActionBarDrawerToggle(this, binding.drawerLayout, R.string.menu_open, R.string.menu_close)
binding.drawerLayout.addDrawerListener(menuBarToggle)
// it's now ready to be used
menuBarToggle.syncState()
// when the menu drawer opens, the toggle button moves to a "back" button and it will close again.
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// handle menu drawer item clicks.
// since these are all events that influence the fragment list, delegate their actions!
binding.navView.setNavigationItemSelectedListener {
when (it.itemId) {
R.id.mnuClear -> clearAllItems()
@ -83,27 +39,21 @@ class MainActivity : AppCompatActivity() {
}
private fun clearAllItems() {
todoFragment.clearAllItems()
//todoFragment.clearAllItems()
}
private fun clearLatestItem() {
todoFragment.clearLatestItem()
//todoFragment.clearLatestItem()
}
private fun resetItems() {
todoFragment.resetItems()
//todoFragment.resetItems()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// we need to do this to respond correctly to clicks on menu items, otherwise it won't be caught
if(menuBarToggle.onOptionsItemSelected(item)) {
return true
}
return super.onOptionsItemSelected(item)
}
fun hideKeyboard(view: View) {
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
}

View File

@ -1,78 +0,0 @@
package be.kuleuven.howlongtobeat
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import be.kuleuven.howlongtobeat.databinding.FragmentTodolistBinding
import be.kuleuven.howlongtobeat.hltb.Game
import be.kuleuven.howlongtobeat.hltb.HLTBClient
import be.kuleuven.howlongtobeat.model.Todo
class TodoFragment : Fragment(R.layout.fragment_todolist) {
private val todoList = arrayListOf<Todo>()
private lateinit var hltbClient: HLTBClient
private lateinit var binding: FragmentTodolistBinding
private lateinit var main: MainActivity
private lateinit var adapter: TodoAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentTodolistBinding.inflate(layoutInflater)
main = activity as MainActivity
adapter = TodoAdapter(todoList)
binding.rvwTodo.adapter = adapter
// If we don't supply a layout manager, the recyclerview will not be displayed
// there are three options here: a simple LinearLayoutManager (1-dimensional), a GridLayoutManager (2D) or a StaggeredGridLayoutManager
binding.rvwTodo.layoutManager = LinearLayoutManager(this.context)
binding.btnAddTodo.setOnClickListener(this::addNewTotoItem)
return binding.root
}
fun onHltbGamesRetrieved(games: List<Game>) {
todoList.clear()
todoList.addAll(games.map { Todo("${it.title} (${it.howlong})", false) })
adapter.notifyDataSetChanged()
}
private fun addNewTotoItem(it: View) {
val newTodoTitle = binding.edtTodo.text.toString()
// this will not automatically updat the view!
todoList.add(Todo(newTodoTitle, false))
adapter.notifyItemInserted(todoList.size - 1)
// adapter.notifyDatasetChanged() also works but will update EVERYTHING, which is not too efficient.
binding.edtTodo.text.clear()
binding.edtTodo.clearFocus()
main.hideKeyboard(it)
}
fun clearAllItems() {
todoList.clear()
adapter.notifyDataSetChanged()
}
fun clearLatestItem() {
if(todoList.size >= 1) {
todoList.removeAt(todoList.size - 1)
adapter.notifyItemRemoved(todoList.size - 1)
}
}
fun resetItems() {
todoList.clear()
todoList.addAll(Todo.defaults())
adapter.notifyDataSetChanged()
}
}

View File

@ -24,10 +24,8 @@ class GoogleVisionClient {
suspend fun findCartCodeViaGoogleVision(cameraSnap: Bitmap): String? {
var response: BatchAnnotateImagesResponse
withContext(Dispatchers.IO) {
println("Encoding image...")
val sml2Data = cameraSnap.asEncodedGoogleVisionImage()
println("Done, uploading image...")
val req = AnnotateImageRequest().apply {
features = listOf(Feature().apply {
type = "TEXT_DETECTION"

View File

@ -1,4 +0,0 @@
package be.kuleuven.howlongtobeat.hltb
data class Game(val title: String, val howlong: Double, val finished: Boolean = false) {
}

View File

@ -5,7 +5,7 @@ import com.android.volley.Response
import com.android.volley.toolbox.StringRequest
import com.android.volley.toolbox.Volley
class HLTBClient(val context: Context, val onResponseFetched: (List<Game>) -> Unit) {
class HLTBClient(val context: Context) {
// Inspired by https://www.npmjs.com/package/howlongtobeat
// The API is abysmal, but hey, it works...
@ -37,7 +37,7 @@ class HLTBClient(val context: Context, val onResponseFetched: (List<Game>) -> Un
}
}
fun triggerFind(query: String) {
fun find(query: String, onResponseFetched: (List<HowLongToBeatResult>) -> Unit) {
val queue = Volley.newRequestQueue(context)
val req = HLTBRequest(query) {
onResponseFetched(HowLongToBeatResultParser.parse(it))

View File

@ -0,0 +1,11 @@
package be.kuleuven.howlongtobeat.hltb
import kotlinx.serialization.Serializable
@Serializable
data class HowLongToBeatResult(val title: String, val howlong: Double, val finished: Boolean = false) : java.io.Serializable {
companion object {
const val RESULT = "HowLongToBeatResult"
const val CODE = "CartCode"
}
}

View File

@ -5,8 +5,8 @@ object HowLongToBeatResultParser {
private val titleMatcher = """<a class=".+" title="(.+)" href=""".toRegex()
private val hourMatcher = """<div class=".+">(.+) Hours""".toRegex()
fun parse(html: String): List<Game> {
val result = arrayListOf<Game>()
fun parse(html: String): List<HowLongToBeatResult> {
val result = arrayListOf<HowLongToBeatResult>()
val rows = html.split("\n")
for(i in 0..rows.size - 1) {
val matched = titleMatcher.find(rows[i])
@ -14,7 +14,7 @@ object HowLongToBeatResultParser {
val (title) = matched.destructured
val hour = parseHoursFromRow(i, rows)
result.add(Game(title, hour))
result.add(HowLongToBeatResult(title, hour))
}
}

View File

@ -0,0 +1,21 @@
package be.kuleuven.howlongtobeat.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class Game(
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "is_done") var isDone: Boolean = false,
@PrimaryKey(autoGenerate = true) var id: Int = 0) : java.io.Serializable {
fun check() {
isDone = true
}
companion object {
val NONE_YET = Game("No entries yet, add one!")
}
}

View File

@ -0,0 +1,8 @@
package be.kuleuven.howlongtobeat.model
interface GameRepository {
fun load(): List<Game>
fun save(items: List<Game>)
}

View File

@ -1,30 +0,0 @@
package be.kuleuven.howlongtobeat.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
// In practice, you should NOT mix seralizable and entity
// This is just an example to show you both Room and ObjectOutputStream's implementations.
@Serializable
@Entity
data class Todo(
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "is_done") var isDone: Boolean,
@PrimaryKey(autoGenerate = true) var id: Int = 0) : java.io.Serializable {
fun check() {
isDone = true
}
companion object {
fun defaults(): List<Todo> = arrayListOf(
Todo("Get graded", false),
Todo("Pay attention", true),
Todo("Get good at Android dev", false),
Todo("Refactor Java projects", false)
)
}
}

View File

@ -1,8 +0,0 @@
package be.kuleuven.howlongtobeat.model
interface TodoRepository {
fun load(): List<Todo>
fun save(items: List<Todo>)
}

View File

@ -1,34 +0,0 @@
package be.kuleuven.howlongtobeat.model.file
import android.content.Context
import be.kuleuven.howlongtobeat.model.Todo
import be.kuleuven.howlongtobeat.model.TodoRepository
import java.io.EOFException
import java.io.FileNotFoundException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
class TodoFileRepository(val context: Context) : TodoRepository {
override fun load(): List<Todo> {
try {
val openFileInput = context.openFileInput("todoitems.txt") ?: return Todo.defaults()
ObjectInputStream(openFileInput).use {
return it.readObject() as ArrayList<Todo>
}
} catch(fileNotFound: FileNotFoundException) {
// no file yet, revert to defaults.
} catch(prematureEndOfFile: EOFException) {
// also ignore this: file incomplete/corrupt, revert to defaults.
}
return Todo.defaults()
}
override fun save(items: List<Todo>) {
val openFileOutput = context.openFileOutput("todoitems.txt", Context.MODE_PRIVATE) ?: return
ObjectOutputStream(openFileOutput).use {
it.writeObject(items)
}
}
}

View File

@ -0,0 +1,23 @@
package be.kuleuven.howlongtobeat.model.room
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import be.kuleuven.howlongtobeat.model.Game
@Dao
interface GameDao {
@Query("SELECT * FROM Game")
fun query(): List<Game>
@Update
fun update(items: List<Game>)
@Query("DELETE FROM Game")
fun deleteAll()
@Insert
fun insert(items: List<Game>)
}

View File

@ -0,0 +1,10 @@
package be.kuleuven.howlongtobeat.model.room
import androidx.room.Database
import androidx.room.RoomDatabase
import be.kuleuven.howlongtobeat.model.Game
@Database(entities = arrayOf(Game::class), version = 1)
abstract class GameDatabase : RoomDatabase() {
abstract fun todoDao() : GameDao
}

View File

@ -2,24 +2,25 @@ package be.kuleuven.howlongtobeat.model.room
import android.content.Context
import androidx.room.Room
import be.kuleuven.howlongtobeat.model.Todo
import be.kuleuven.howlongtobeat.model.TodoRepository
import be.kuleuven.howlongtobeat.model.Game
import be.kuleuven.howlongtobeat.model.GameRepository
class TodoRoomRepository(appContext: Context) : TodoRepository {
class GameRepositoryRoomImpl(appContext: Context) :
GameRepository {
private val db: TodoDatabase
private val dao: TodoDao
private val db: GameDatabase
private val dao: GameDao
init {
db = Room.databaseBuilder(appContext, TodoDatabase::class.java, "todo-db")
db = Room.databaseBuilder(appContext, GameDatabase::class.java, "todo-db")
.allowMainThreadQueries()
.build()
dao = db.todoDao()
}
override fun load(): List<Todo> = dao.query()
override fun load(): List<Game> = dao.query()
override fun save(items: List<Todo>) {
override fun save(items: List<Game>) {
// You'll learn more about transactions in the database course in the 3rd academic year.
db.runInTransaction {
dao.deleteAll()

View File

@ -1,23 +0,0 @@
package be.kuleuven.howlongtobeat.model.room
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import be.kuleuven.howlongtobeat.model.Todo
@Dao
interface TodoDao {
@Query("SELECT * FROM Todo")
fun query(): List<Todo>
@Update
fun update(items: List<Todo>)
@Query("DELETE FROM Todo")
fun deleteAll()
@Insert
fun insert(items: List<Todo>)
}

View File

@ -1,10 +0,0 @@
package be.kuleuven.howlongtobeat.model.room
import androidx.room.Database
import androidx.room.RoomDatabase
import be.kuleuven.howlongtobeat.model.Todo
@Database(entities = arrayOf(Todo::class), version = 1)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao() : TodoDao
}

View File

@ -7,10 +7,17 @@
android:id="@+id/drawerLayout"
tools:context=".MainActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/frmTodoContainer" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:navGraph="@navigation/nav_graph"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"

View File

@ -1,33 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvwTodo"
android:id="@+id/rvGameList"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/edtTodo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/edtTodo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="new TODO"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnAddTodo"
app:layout_constraintStart_toStartOf="parent" />
<Button
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnAddTodo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:src="@android:drawable/ic_menu_add"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="Add New Game"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvHltbResult"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -19,6 +19,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Please wait, fetching..."
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/indeterminateBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="16dp">
<TextView
android:id="@+id/txtHltbItemResult"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Some Result"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/gameListFragment">
<fragment
android:id="@+id/gameListFragment"
android:name="be.kuleuven.howlongtobeat.GameListFragment"
android:label="GameListFragment" >
<action
android:id="@+id/action_gameListFragment_to_loadingFragment"
app:destination="@id/loadingFragment" />
</fragment>
<fragment
android:id="@+id/loadingFragment"
android:name="be.kuleuven.howlongtobeat.LoadingFragment"
android:label="LoadingFragment" >
<action
android:id="@+id/action_loadingFragment_to_hltbResultsFragment"
app:destination="@id/hltbResultsFragment" />
</fragment>
<fragment
android:id="@+id/hltbResultsFragment"
android:name="be.kuleuven.howlongtobeat.HltbResultsFragment"
android:label="HltbResultsFragment" />
</navigation>