Skip to content

Commit

Permalink
Add a board selection screen to the Dungeon sample.
Browse files Browse the repository at this point in the history
  • Loading branch information
zach-klippenstein committed Jan 14, 2020
1 parent 344ec4d commit 5336c42
Show file tree
Hide file tree
Showing 19 changed files with 595 additions and 129 deletions.
1 change: 1 addition & 0 deletions kotlin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ buildscript {
classpath deps.kotlin.gradlePlugin
classpath deps.ktlint
classpath deps.mavenPublish
classpath "org.jetbrains.kotlin:kotlin-serialization:1.3.60"
}

repositories {
Expand Down
1 change: 1 addition & 0 deletions kotlin/samples/dungeon/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies {
implementation deps.rxandroid2
implementation deps.rxjava2.rxjava2
implementation deps.timber
implementation "com.squareup.cycler:cycler:0.1.0"

testImplementation deps.test.junit
testImplementation deps.test.truth
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
---
name: Simple Board
---
🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳
🌳 🌳
🌳 🌳
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
---
name: Simple Maze
---
┌──────────────┐
│ │
│ │
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,46 +26,85 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okio.buffer
import okio.source
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.seconds

/**
* Service class that creates [Worker]s to [load][load] [Board]s.
* Service class that creates [Worker]s to [load][loadBoard] [Board]s.
*/
@UseExperimental(ExperimentalTime::class)
class BoardLoader(
private val ioDispatcher: CoroutineDispatcher,
private val assets: AssetManager
private val assets: AssetManager,
private val boardsAssetPath: String
) {

/**
* Returns a [Worker] that will read and parse the [Board] located at [path].
*
* Workers created for the same path will be considered [equivalent][Worker.doesSameWorkAs].
*/
fun load(path: String): Worker<Board> = BoardLoaderWorker(path)
private inner class BoardListLoaderWorker : Worker<Map<String, Board>> {
override fun run(): Flow<Map<String, Board>> = flow {
val boards = withMinimumDelay(1.seconds) {
withContext(ioDispatcher) {
loadBoardsBlocking()
}
}
emit(boards)
}
}

private inner class BoardLoaderWorker(private val path: String) : Worker<Board> {
private inner class BoardLoaderWorker(private val filename: String) : Worker<Board> {
override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean =
otherWorker is BoardLoaderWorker && filename == otherWorker.filename

override fun run(): Flow<Board> = flow {
val board = coroutineScope {
// Wait at least a second before emitting to make it look like we're doing real work.
// Structured concurrency means this coroutineScope block won't return until this delay
// finishes.
launch { delay(1000) }

val board = withMinimumDelay(1.seconds) {
withContext(ioDispatcher) {
@Suppress("BlockingMethodInNonBlockingContext")
assets.open(path)
.use {
it.source()
.parseBoard()
}
loadBoardBlocking(filename)
}
}
emit(board)
}
}

override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean {
return otherWorker is BoardLoaderWorker && path == otherWorker.path
}
fun loadAvailableBoards(): Worker<Map<String, Board>> = BoardListLoaderWorker()

/**
* Returns a [Worker] that will read and parse the [Board] located at [filename].
*
* Workers created for the same path will be considered [equivalent][Worker.doesSameWorkAs].
*/
fun loadBoard(filename: String): Worker<Board> = BoardLoaderWorker(filename)

/**
* Runs [block] and returns the value it returned, but will not return (by suspending) for at
* least [delay] period of time. Used to add fake delays to demonstrate loading states.
*/
private suspend inline fun <T> withMinimumDelay(
delay: Duration,
crossinline block: suspend () -> T
): T = coroutineScope {
// Wait at least a second before emitting to make it look like we're doing real work.
// Structured concurrency means this coroutineScope block won't return until this delay
// finishes.
launch { delay(delay.toLongMilliseconds()) }

block()
}

@Suppress("UNCHECKED_CAST")
private fun loadBoardsBlocking(): Map<String, Board> {
val boardFiles = assets.list(boardsAssetPath)!!.asList()
return boardFiles.associateWith { filename -> loadBoardBlocking(filename) }
}

private fun loadBoardBlocking(filename: String): Board =
assets.open(absoluteBoardPath(filename))
.use {
it.source()
.buffer()
.parseBoard()
}

private fun absoluteBoardPath(filename: String) = "$boardsAssetPath/$filename"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2020 Square Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.sample.dungeon

import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.squareup.cycler.Recycler
import com.squareup.cycler.toDataSource
import com.squareup.sample.dungeon.DungeonAppWorkflow.DisplayBoardsListScreen
import com.squareup.sample.dungeon.board.Board
import com.squareup.sample.todo.R
import com.squareup.workflow.ui.ContainerHints
import com.squareup.workflow.ui.LayoutRunner
import com.squareup.workflow.ui.LayoutRunner.Companion.bind
import com.squareup.workflow.ui.ViewBinding
import com.squareup.workflow.ui.WorkflowViewStub

/**
* TODO write documentation
*/
class BoardsListLayoutRunner(view: View) : LayoutRunner<DisplayBoardsListScreen> {

private var onBoardSelected: (index: Int) -> Unit = {}

private val recycler =
Recycler.adopt<Pair<Board, ContainerHints>>(view.findViewById(R.id.boards_list_recycler)) {
row<Pair<Board, ContainerHints>, ViewGroup> {
create(R.layout.boards_list_item) {
bind { index, (board, containerHints) ->
val card: CardView = this.view.findViewById(R.id.board_card)
val boardNameView: TextView = this.view.findViewById(R.id.board_name)
val boardPreviewView: WorkflowViewStub = this.view.findViewById(R.id.board_preview)

boardNameView.text = board.metadata.name
boardPreviewView.update(board, containerHints)
card.setOnClickListener { onBoardSelected(index) }
}
}
}
}

override fun showRendering(
rendering: DisplayBoardsListScreen,
containerHints: ContainerHints
) {
// Associate the containerHints to each item so it can be used when binding the RecyclerView
// item.
recycler.data = rendering.boards
.map { it to containerHints }
.toDataSource()
onBoardSelected = rendering.onBoardSelected
}

companion object : ViewBinding<DisplayBoardsListScreen> by bind(
R.layout.boards_list_layout, ::BoardsListLayoutRunner
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ package com.squareup.sample.dungeon

import android.content.Context
import android.os.Vibrator
import com.squareup.sample.dungeon.DungeonAppWorkflow.State.LoadingBoardList
import com.squareup.sample.dungeon.GameSessionWorkflow.State.Loading
import com.squareup.sample.timemachine.shakeable.ShakeableTimeMachineLayoutRunner
import com.squareup.sample.todo.R
import com.squareup.workflow.ui.ViewRegistry
import kotlinx.coroutines.Dispatchers
import kotlin.random.Random
Expand All @@ -31,10 +34,12 @@ private const val AI_COUNT = 4
class Component(context: Context) {

val viewRegistry = ViewRegistry(
BoardView,
ShakeableTimeMachineLayoutRunner,
LoadingBinding<LoadingBoardList>(R.string.loading_boards_list),
BoardsListLayoutRunner,
LoadingBinding<Loading>(R.string.loading_board),
GameLayoutRunner,
LoadingBoardBinding,
ShakeableTimeMachineLayoutRunner
BoardView
)

val random = Random(System.currentTimeMillis())
Expand All @@ -44,15 +49,17 @@ class Component(context: Context) {

val vibrator = context.getSystemService(Vibrator::class.java)!!

val boardLoader = BoardLoader(Dispatchers.IO, context.assets)
val boardLoader = BoardLoader(Dispatchers.IO, context.assets, boardsAssetPath = "boards")

val playerWorkflow = PlayerWorkflow()

val aiWorkflows = List(AI_COUNT) { AiWorkflow(random = random) }

val gameWorkflow = GameWorkflow(playerWorkflow, aiWorkflows, random)

val appWorkflow = DungeonAppWorkflow(gameWorkflow, vibrator, boardLoader)
val gameSessionWorkflow = GameSessionWorkflow(gameWorkflow, vibrator, boardLoader)

val appWorkflow = DungeonAppWorkflow(gameSessionWorkflow, boardLoader)

val timeMachineWorkflow = TimeMachineAppWorkflow(appWorkflow, clock, context)
}
Loading

0 comments on commit 5336c42

Please sign in to comment.