├── .gitignore
├── README.md
├── modules
├── android
│ ├── build.sbt
│ ├── project
│ │ └── plugins.sbt
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── res
│ │ ├── drawable-hdpi
│ │ │ ├── ic_add.png
│ │ │ └── icon_app.png
│ │ ├── drawable-mdpi
│ │ │ └── icon_app.png
│ │ ├── drawable-xhdpi
│ │ │ ├── ic_add.png
│ │ │ └── icon_app.png
│ │ ├── drawable-xxhdpi
│ │ │ ├── ic_add.png
│ │ │ └── icon_app.png
│ │ ├── drawable-xxxhdpi
│ │ │ └── icon_app.png
│ │ ├── layout
│ │ │ ├── image_item.xml
│ │ │ └── material_list_activity.xml
│ │ └── values
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ ├── styles.xml
│ │ │ └── themes.xml
│ │ └── scala
│ │ └── com
│ │ └── fortysevendeg
│ │ └── architecture
│ │ └── ui
│ │ ├── commons
│ │ ├── AsyncImageTweaks.scala
│ │ ├── CommonsStyles.scala
│ │ ├── FABAnimationBehavior.scala
│ │ ├── UiExceptions.scala
│ │ └── UiOps.scala
│ │ ├── components
│ │ └── CircularTransformation.scala
│ │ └── main
│ │ ├── MainActivity.scala
│ │ ├── adapters
│ │ └── AnimalsAdapter.scala
│ │ ├── holders
│ │ └── AnimalViewHolder.scala
│ │ └── jobs
│ │ ├── MainDom.scala
│ │ ├── MainJobs.scala
│ │ └── MainListUiActions.scala
├── commons
│ └── src
│ │ └── main
│ │ └── scala
│ │ └── commons
│ │ ├── AppLog.scala
│ │ ├── CatchAll.scala
│ │ ├── TaskServiceOps.scala
│ │ └── package.scala
└── services
│ └── src
│ └── main
│ └── scala
│ └── com
│ └── fortysevendeg
│ └── architecture
│ └── services
│ └── api
│ ├── ApiService.scala
│ ├── Exceptions.scala
│ ├── Models.scala
│ └── impl
│ └── ApiServiceImpl.scala
├── project
├── AppBuild.scala
├── Libraries.scala
├── Settings.scala
├── Versions.scala
├── build.properties
└── plugins.sbt
└── resources
└── architecture.png
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | logs
3 | target
4 | tmp
5 | .history
6 | dist
7 | /out
8 | /RUNNING_PID
9 | /.ivy*
10 |
11 | # sbt specific
12 | /.sbt
13 | .cache/
14 | .history/
15 | .lib/
16 | dist/*
17 | target/
18 | lib_managed/
19 | src_managed/
20 | project/boot/
21 | project/plugins/project/
22 | project/project
23 | project/target
24 | /.activator
25 |
26 | # Scala-IDE specific
27 | .scala_dependencies
28 | .worksheet
29 |
30 | #Eclipse specific
31 | .classpath
32 | .project
33 | .cache
34 | .settings/
35 |
36 | #IntelliJ IDEA specific
37 | .idea/
38 | /.idea_modules
39 | /.idea
40 | /*.iml
41 |
42 | #Proguard
43 | proguard-sbt.txt
44 |
45 | #Properties
46 | local.properties
47 | debug.properties
48 | release.properties
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple Scala Android Architecture
2 |
3 | This is a simple architecture for Android project made in Scala using Cats and ScalaZ libraries
4 |
5 | # Modules
6 |
7 | - **android**: This module contains the Android SDK with Activities, Fragments and so on, used
8 | in your project. Every screen have _jobs_, with the actions in your UI and _Ui Actions_
9 | (We speak about them later)
10 |
11 | - **services**: This module contains services for connecting to out of the applications. For
12 | example: API, Repository, Disk, so on
13 |
14 | - **commons**: This module contains types and resources used in other module in order to compose
15 | the result of the methods
16 |
17 | # Architecture
18 |
19 | Our Activities, Fragment and other screen of Android call to action using _Jobs_. Jobs are a
20 | group of methods that contain the things that the UI can do. For example: _loadItems_,
21 | _showItem_, _markAsDone_, etc
22 |
23 | The principles of the Jobs is that they can connect to the UI (using _Ui Actions_) and api, repository
24 | or whatever (using _Services_)
25 |
26 | 
27 |
28 | In order to can compose the methods of the Ui and Services, all methods must return the same type.
29 | The type is define in _commons_ module and it's the next:
30 |
31 | _**type TaskService[A] = XorT[Task, ServiceException, A]**_
32 |
33 | Our _TaskService_ type is a _Task_ of _ScalaZ_ in other to can do async tasks and using a _Xor_ of
34 | _Cats_ for exceptions and value of the method
35 |
36 | For example, a method of our Job can have calls to Ui and Services:
37 |
38 | ```scala
39 | def loadAnimals: TaskService[Unit] = {
40 | for {
41 | _ <- uiActions.showLoading()
42 | animals <- apiService.getAnimals()
43 | _ <- uiActions.showContent()
44 | _ <- uiActions.loadAnimals(animals)
45 | } yield ()
46 | }
47 | ```
48 |
49 | In the activity we can do that:
50 |
51 | ```scala
52 | val tasks = (jobs.initialize |@| jobs.loadAnimals).tupled
53 |
54 | tasks.resolveServiceOr(_ => jobs.showError)
55 | ```
56 |
57 | We can compose _initialize_ and _loadAnimals_ in a _Applicative_ and using _TaskOps_ (defined in _commons_
58 | module) we can launch the async task and launch the error if the task doesn't work
--------------------------------------------------------------------------------
/modules/android/build.sbt:
--------------------------------------------------------------------------------
1 | platformTarget in Android := "android-23"
--------------------------------------------------------------------------------
/modules/android/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | logLevel := Level.Info
2 | addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.19")
3 |
--------------------------------------------------------------------------------
/modules/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/modules/android/src/main/res/drawable-hdpi/ic_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/47degrees/scala-android-architecture/c4452b1957f5cee067b1bbcf221489dce5efa31d/modules/android/src/main/res/drawable-hdpi/ic_add.png
--------------------------------------------------------------------------------
/modules/android/src/main/res/drawable-hdpi/icon_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/47degrees/scala-android-architecture/c4452b1957f5cee067b1bbcf221489dce5efa31d/modules/android/src/main/res/drawable-hdpi/icon_app.png
--------------------------------------------------------------------------------
/modules/android/src/main/res/drawable-mdpi/icon_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/47degrees/scala-android-architecture/c4452b1957f5cee067b1bbcf221489dce5efa31d/modules/android/src/main/res/drawable-mdpi/icon_app.png
--------------------------------------------------------------------------------
/modules/android/src/main/res/drawable-xhdpi/ic_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/47degrees/scala-android-architecture/c4452b1957f5cee067b1bbcf221489dce5efa31d/modules/android/src/main/res/drawable-xhdpi/ic_add.png
--------------------------------------------------------------------------------
/modules/android/src/main/res/drawable-xhdpi/icon_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/47degrees/scala-android-architecture/c4452b1957f5cee067b1bbcf221489dce5efa31d/modules/android/src/main/res/drawable-xhdpi/icon_app.png
--------------------------------------------------------------------------------
/modules/android/src/main/res/drawable-xxhdpi/ic_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/47degrees/scala-android-architecture/c4452b1957f5cee067b1bbcf221489dce5efa31d/modules/android/src/main/res/drawable-xxhdpi/ic_add.png
--------------------------------------------------------------------------------
/modules/android/src/main/res/drawable-xxhdpi/icon_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/47degrees/scala-android-architecture/c4452b1957f5cee067b1bbcf221489dce5efa31d/modules/android/src/main/res/drawable-xxhdpi/icon_app.png
--------------------------------------------------------------------------------
/modules/android/src/main/res/drawable-xxxhdpi/icon_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/47degrees/scala-android-architecture/c4452b1957f5cee067b1bbcf221489dce5efa31d/modules/android/src/main/res/drawable-xxxhdpi/icon_app.png
--------------------------------------------------------------------------------
/modules/android/src/main/res/layout/image_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
22 |
23 |
28 |
29 |
35 |
36 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/modules/android/src/main/res/layout/material_list_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
25 |
26 |
32 |
33 |
38 |
39 |
44 |
45 |
52 |
53 |
54 |
55 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/modules/android/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | #2D4053
22 | #ff192533
23 | #F2554A
24 | #ff49cb94
25 |
26 | #87364550
27 | #ffffff
28 | #E05F5E
29 | #ffc45453
30 |
31 |
--------------------------------------------------------------------------------
/modules/android/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 | 3dp
22 | 2dp
23 | 4dp
24 | 8dp
25 | 8dp
26 | 16dp
27 |
28 | 2dp
29 | 4dp
30 |
31 | 1dp
32 |
33 | 10sp
34 | 12sp
35 | 14sp
36 | 16sp
37 | 20sp
38 | 24sp
39 |
40 | 56dp
41 |
42 | 2dp
43 |
44 | 200dp
45 |
46 |
47 |
--------------------------------------------------------------------------------
/modules/android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 | Scala Architecture
21 |
22 | Add new item
23 |
24 | Error
25 |
26 |
27 |
--------------------------------------------------------------------------------
/modules/android/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/modules/android/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/commons/AsyncImageTweaks.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.fortysevendeg.architecture.ui.commons
17 |
18 | import android.widget.ImageView
19 | import com.fortysevendeg.macroid.extras.DeviceVersion._
20 | import com.fortysevendeg.macroid.extras.ViewTweaks._
21 | import com.fortysevendeg.architecture.ui.components.CircularTransformation
22 |
23 | import com.squareup.picasso.Picasso
24 | import macroid.{ContextWrapper, Tweak}
25 |
26 | import scala.language.postfixOps
27 |
28 | object AsyncImageTweaks {
29 | type W = ImageView
30 |
31 | def roundedImage(url: String,
32 | placeHolder: Int,
33 | size: Int)(implicit context: ContextWrapper) = CurrentVersion match {
34 | case sdk if sdk >= Lollipop =>
35 | srcImage(url, placeHolder) + vCircleOutlineProvider(0)
36 | case _ =>
37 | roundedImageTweak(url, placeHolder, size)
38 | }
39 |
40 | private def roundedImageTweak(
41 | url: String,
42 | placeHolder: Int,
43 | size: Int
44 | )(implicit context: ContextWrapper): Tweak[W] = Tweak[W](
45 | imageView => {
46 | Picasso.`with`(context.getOriginal)
47 | .load(url)
48 | .transform(new CircularTransformation(size))
49 | .placeholder(placeHolder)
50 | .into(imageView)
51 | }
52 | )
53 |
54 | def srcImage(
55 | url: String,
56 | placeHolder: Int
57 | )(implicit context: ContextWrapper): Tweak[W] = Tweak[W](
58 | imageView => {
59 | Picasso.`with`(context.getOriginal)
60 | .load(url)
61 | .placeholder(placeHolder)
62 | .into(imageView)
63 | }
64 | )
65 |
66 | def srcImage(url: String)(implicit context: ContextWrapper): Tweak[W] = Tweak[W](
67 | imageView => {
68 | Picasso.`with`(context.getOriginal)
69 | .load(url)
70 | .into(imageView)
71 | }
72 | )
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/commons/CommonsStyles.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.commons
18 |
19 | import com.fortysevendeg.architecture.R
20 | import com.fortysevendeg.macroid.extras.ViewTweaks._
21 |
22 | object CommonsStyles {
23 |
24 | val toolbarStyle =
25 | vBackground(R.color.primary) +
26 | vMatchWidth
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/commons/FABAnimationBehavior.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.commons
18 |
19 | import android.animation.{Animator, AnimatorListenerAdapter}
20 | import android.content.Context
21 | import android.support.design.widget.{CoordinatorLayout, FloatingActionButton}
22 | import android.support.v4.view.ViewCompat
23 | import android.support.v4.view.animation.FastOutSlowInInterpolator
24 | import android.util.AttributeSet
25 | import android.view.View
26 | import macroid.{Snail, _}
27 |
28 | import scala.concurrent.ExecutionContext.Implicits.global
29 | import scala.concurrent.Promise
30 |
31 | class FABAnimationBehavior
32 | extends FloatingActionButton.Behavior {
33 |
34 | def this(context: Context, attrs: AttributeSet) = this()
35 |
36 | var isAnimatingOut = false
37 |
38 | val interpolator = new FastOutSlowInInterpolator()
39 |
40 | val duration = 200L
41 |
42 | override def onStartNestedScroll(
43 | coordinatorLayout: CoordinatorLayout,
44 | child: FloatingActionButton,
45 | directTargetChild: View,
46 | target: View,
47 | nestedScrollAxes: Int): Boolean =
48 | nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
49 | super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes)
50 |
51 | override def onNestedScroll(
52 | coordinatorLayout: CoordinatorLayout,
53 | child: FloatingActionButton,
54 | target: View,
55 | dxConsumed: Int,
56 | dyConsumed: Int,
57 | dxUnconsumed: Int,
58 | dyUnconsumed: Int): Unit = {
59 | super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)
60 |
61 | (dyConsumed, child) match {
62 | case (d, c) if d > 0 && !isAnimatingOut && c.getVisibility == View.VISIBLE =>
63 | (Option(child) <~~ animateOut).run
64 | case (d, c) if d < 0 && c.getVisibility != View.VISIBLE =>
65 | (Option(child) <~~ animateIn).run
66 | case _ =>
67 | }
68 | }
69 |
70 | val animateIn = Snail[FloatingActionButton] {
71 | view ⇒
72 | view.setVisibility(View.VISIBLE)
73 | val animPromise = Promise[Unit]()
74 | view.animate
75 | .translationY(0)
76 | .setInterpolator(interpolator)
77 | .setDuration(duration)
78 | .setListener(new AnimatorListenerAdapter {
79 | override def onAnimationEnd(animation: Animator) {
80 | super.onAnimationEnd(animation)
81 | animPromise.success()
82 | }
83 | }).start()
84 | animPromise.future
85 | }
86 |
87 | val animateOut = Snail[FloatingActionButton] {
88 | view ⇒
89 | val animPromise = Promise[Unit]()
90 | val y = view.getHeight + (view.getPaddingBottom * 2)
91 | view.animate
92 | .translationY(y)
93 | .setInterpolator(interpolator)
94 | .setDuration(duration)
95 | .setListener(new AnimatorListenerAdapter {
96 | override def onAnimationStart(animation: Animator): Unit = {
97 | super.onAnimationStart(animation)
98 | isAnimatingOut = true
99 | }
100 | override def onAnimationCancel(animation: Animator): Unit = {
101 | super.onAnimationCancel(animation)
102 | isAnimatingOut = false
103 | }
104 | override def onAnimationEnd(animation: Animator) {
105 | super.onAnimationEnd(animation)
106 | isAnimatingOut = false
107 | view.setVisibility(View.GONE)
108 | animPromise.success()
109 | }
110 | }).start()
111 | animPromise.future
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/commons/UiExceptions.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.commons
18 |
19 | import commons.TaskService.ServiceException
20 |
21 | case class UiException(message: String, cause: Option[Throwable] = None)
22 | extends RuntimeException(message)
23 | with ServiceException {
24 | cause map initCause
25 | }
26 |
27 | trait ImplicitsUiExceptions {
28 | implicit def uiExceptionConverter = (t: Throwable) => UiException(t.getMessage, Option(t))
29 | }
30 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/commons/UiOps.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.commons
18 |
19 | import commons.TaskService.TaskService
20 | import commons.{CatchAll, TaskService}
21 | import macroid.Ui
22 | import monix.eval.Task
23 |
24 | object UiOps extends ImplicitsUiExceptions {
25 |
26 | implicit class ServiceUi(ui: Ui[Any]) {
27 |
28 | def toService: TaskService[Unit] = TaskService {
29 | Task(CatchAll[UiException](ui.run))
30 | }
31 |
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/components/CircularTransformation.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.components
18 |
19 | import android.graphics.{Bitmap, Canvas, Paint, PorterDuff, PorterDuffXfermode, Rect}
20 |
21 | import com.squareup.picasso.Transformation
22 |
23 | class CircularTransformation(size: Int)
24 | extends Transformation {
25 |
26 | val radius = Math.ceil(size / 2).toInt
27 |
28 | def transform(source: Bitmap): Bitmap = {
29 | val output: Bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
30 | val canvas: Canvas = new Canvas(output)
31 | val color: Int = 0xff424242
32 | val paint: Paint = new Paint
33 | val rect: Rect = new Rect(0, 0, source.getWidth, source.getHeight)
34 | val target: Rect = new Rect(0, 0, size, size)
35 | paint.setAntiAlias(true)
36 | canvas.drawARGB(0, 0, 0, 0)
37 | paint.setColor(color)
38 | canvas.drawCircle(radius, radius, radius, paint)
39 | paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
40 | canvas.drawBitmap(source, rect, target, paint)
41 | source.recycle()
42 | output
43 | }
44 |
45 | def key: String = {
46 | s"radius-$size"
47 | }
48 | }
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/main/MainActivity.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.main
18 |
19 | import android.os.Bundle
20 | import android.support.v7.app.AppCompatActivity
21 | import cats.implicits._
22 | import com.fortysevendeg.architecture.ui.main.jobs.{MainDom, MainJobs, MainListUiActions}
23 | import com.fortysevendeg.architecture.{R, TypedFindView}
24 | import commons.TaskService._
25 | import commons.TaskServiceOps._
26 | import macroid.Contexts
27 |
28 | class MainActivity
29 | extends AppCompatActivity
30 | with TypedFindView
31 | with Contexts[AppCompatActivity] {
32 |
33 | lazy val dom = MainDom(this)
34 |
35 | lazy val ui = MainListUiActions(dom)
36 |
37 | lazy val jobs = new MainJobs(ui)
38 |
39 | override def onCreate(savedInstanceState: Bundle) = {
40 | super.onCreate(savedInstanceState)
41 |
42 | setContentView(R.layout.material_list_activity)
43 |
44 | val tasks = (jobs.initialize |@| jobs.loadAnimals).tupled
45 |
46 | tasks.resolveServiceOr(_ => jobs.showError)
47 |
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/main/adapters/AnimalsAdapter.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.main.adapters
18 |
19 | import android.support.v7.widget.RecyclerView
20 | import android.view.{LayoutInflater, ViewGroup}
21 | import com.fortysevendeg.architecture.R
22 | import com.fortysevendeg.architecture.services.api.Animal
23 | import com.fortysevendeg.architecture.ui.main.holders.AnimalViewHolder
24 | import macroid._
25 |
26 | case class AnimalsAdapter(animals: Seq[Animal])(implicit context: ContextWrapper)
27 | extends RecyclerView.Adapter[AnimalViewHolder] {
28 |
29 | override def onCreateViewHolder(parent: ViewGroup, i: Int): AnimalViewHolder = {
30 | val v = LayoutInflater.from(parent.getContext).inflate(R.layout.image_item, parent, false)
31 | new AnimalViewHolder(v)
32 | }
33 |
34 | override def getItemCount: Int = animals.size
35 |
36 | override def onBindViewHolder(viewHolder: AnimalViewHolder, position: Int): Unit =
37 | viewHolder.bind(animals(position))
38 |
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/main/holders/AnimalViewHolder.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.main.holders
18 |
19 | import android.support.v7.widget.RecyclerView
20 | import android.view.View
21 | import com.fortysevendeg.architecture.services.api.Animal
22 | import com.fortysevendeg.architecture.ui.commons.AsyncImageTweaks._
23 | import com.fortysevendeg.architecture.{TR, TypedFindView}
24 | import com.fortysevendeg.macroid.extras.TextTweaks._
25 | import com.fortysevendeg.macroid.extras.ViewTweaks._
26 | import macroid.FullDsl._
27 | import macroid._
28 |
29 | case class AnimalViewHolder(parent: View)(implicit cw: ContextWrapper)
30 | extends RecyclerView.ViewHolder(parent)
31 | with TypedFindView {
32 |
33 | lazy val image = Option(findView(TR.image))
34 |
35 | lazy val text = Option(findView(TR.text))
36 |
37 | override protected def findViewById(id: Int): View = parent.findViewById(id)
38 |
39 | def bind(animal: Animal): Unit =
40 | ((parent <~ On.click(parent <~ vSnackbarLong(animal.name))) ~
41 | (text <~ tvText(animal.name)) ~
42 | (image <~ srcImage(animal.url))).run
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/main/jobs/MainDom.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.main.jobs
18 |
19 | import com.fortysevendeg.architecture.{TR, TypedFindView}
20 | import macroid.ActivityContextWrapper
21 |
22 | case class MainDom(fv: TypedFindView)(implicit val contextWrapper: ActivityContextWrapper) {
23 |
24 | lazy val content = Option(fv.findView(TR.content))
25 |
26 | lazy val toolBar = Option(fv.findView(TR.toolbar))
27 |
28 | lazy val appBarLayout = Option(fv.findView(TR.app_bar_layout))
29 |
30 | lazy val recycler = Option(fv.findView(TR.recycler))
31 |
32 | lazy val fabActionButton = Option(fv.findView(TR.fab_action_button))
33 |
34 | lazy val loading = Option(fv.findView(TR.loading))
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/main/jobs/MainJobs.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.main.jobs
18 |
19 | import cats.implicits._
20 | import com.fortysevendeg.architecture.services.api.impl.ApiServiceImpl
21 | import commons.TaskService
22 | import commons.TaskService._
23 | import macroid.ActivityContextWrapper
24 |
25 | class MainJobs(ui: MainListUiActions)(implicit activityContextWrapper: ActivityContextWrapper) {
26 |
27 | val apiService = new ApiServiceImpl
28 |
29 | def initialize: TaskService[Unit] = ui.init(this)
30 |
31 | def loadAnimals: TaskService[Unit] = {
32 | for {
33 | _ <- ui.showLoading()
34 | animals <- apiService.getAnimals()
35 | _ <- ui.showContent()
36 | _ <- ui.loadAnimals(animals)
37 | } yield ()
38 | }
39 |
40 | def addItem(): TaskService[Unit] = ui.addItem()
41 |
42 | def showError: TaskService[(Unit, Unit)] =
43 | TaskService((ui.showError() |@| ui.displayError()).tupled.value)
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/modules/android/src/main/scala/com/fortysevendeg/architecture/ui/main/jobs/MainListUiActions.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.ui.main.jobs
18 |
19 | import android.support.v7.app.AppCompatActivity
20 | import android.support.v7.widget.GridLayoutManager
21 | import com.fortysevendeg.architecture.R
22 | import com.fortysevendeg.architecture.services.api.Animal
23 | import com.fortysevendeg.architecture.ui.commons.UiOps._
24 | import com.fortysevendeg.architecture.ui.main.adapters.AnimalsAdapter
25 | import com.fortysevendeg.macroid.extras.ImageViewTweaks._
26 | import com.fortysevendeg.macroid.extras.RecyclerViewTweaks._
27 | import com.fortysevendeg.macroid.extras.ViewTweaks._
28 | import commons.TaskService._
29 | import commons.TaskServiceOps._
30 | import macroid.FullDsl._
31 | import macroid._
32 |
33 | import scala.language.postfixOps
34 |
35 | case class MainListUiActions(dom: MainDom)(implicit contextWrapper: ContextWrapper) {
36 |
37 | def init(jobs: MainJobs): TaskService[Unit] =
38 | (Ui {
39 | (contextWrapper.original.get, dom.toolBar) match {
40 | case (Some(activity: AppCompatActivity), Some(tb)) =>
41 | activity.setSupportActionBar(tb)
42 | case _ =>
43 | }
44 | } ~
45 | (dom.recycler
46 | <~ rvFixedSize
47 | <~ rvLayoutManager(new GridLayoutManager(contextWrapper.bestAvailable, 2))) ~
48 | (dom.fabActionButton
49 | <~ ivSrc(R.drawable.ic_add)
50 | <~ On.click(Ui(jobs.addItem().resolveAsync())))).toService
51 |
52 | def loadAnimals(data: Seq[Animal]): TaskService[Unit] =
53 | (dom.recycler <~ rvAdapter(AnimalsAdapter(data))).toService
54 |
55 | def addItem(): TaskService[Unit] =
56 | (dom.content <~ vSnackbarLong(R.string.material_list_add_item)).toService
57 |
58 | def displayError(): TaskService[Unit] =
59 | (dom.content <~ vSnackbarIndefinite(R.string.material_list_error)).toService
60 |
61 | def showLoading(): TaskService[Unit] =
62 | ((dom.loading <~ vVisible) ~
63 | (dom.recycler <~ vGone)).toService
64 |
65 | def showContent(): TaskService[Unit] =
66 | ((dom.loading <~ vGone) ~
67 | (dom.recycler <~ vVisible)).toService
68 |
69 | def showError(): TaskService[Unit] =
70 | ((dom.loading <~ vGone) ~
71 | (dom.recycler <~ vGone)).toService
72 |
73 | }
74 |
75 |
76 |
--------------------------------------------------------------------------------
/modules/commons/src/main/scala/commons/AppLog.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package commons
18 |
19 | object AppLog {
20 |
21 | val tag = "scala_architecture"
22 |
23 | def printErrorMessage(ex: Throwable, message: Option[String] = None) = {
24 | try {
25 | val outputEx = Option(ex.getCause) getOrElse ex
26 | println(s"$tag - ${message getOrElse errorMessage(outputEx)} $outputEx")
27 | } catch { case _: Throwable => }
28 | }
29 |
30 | def printErrorTaskMessage(header: String, ex: Throwable) = {
31 | try {
32 | println(s"$tag - $header")
33 | printErrorMessage(ex, Some(errorMessage(ex)))
34 | } catch { case _: Throwable => }
35 | }
36 |
37 | private[this] def errorMessage(ex: Throwable): String =
38 | Option(ex.getMessage) getOrElse ex.getClass.toString
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/modules/commons/src/main/scala/commons/CatchAll.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package commons
18 |
19 | import cats.syntax.either._
20 |
21 | object CatchAll {
22 |
23 | def apply[E] = new CatchingAll[E]()
24 |
25 | class CatchingAll[E] {
26 | def apply[V](f: => V)(implicit converter: Throwable => E): Either[E, V] =
27 | Either.catchNonFatal(f) leftMap converter
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/modules/commons/src/main/scala/commons/TaskServiceOps.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package commons
18 |
19 | import cats.syntax.either._
20 | import commons.AppLog._
21 | import commons.TaskService.{ServiceException, TaskService}
22 | import monix.eval.Task
23 | import monix.execution.Scheduler.Implicits.global
24 |
25 | import scala.util.{Either, Failure, Success}
26 |
27 | object TaskServiceOps {
28 |
29 | implicit class TaskServiceUi[A](t: TaskService[A]) {
30 |
31 | def resolveAsync[E >: Throwable](
32 | onResult: A => Unit = a => (),
33 | onException: E => Unit = (e: Throwable) => ()
34 | ): Unit = {
35 | Task.fork(t.value).runAsync { result =>
36 | result match {
37 | case Failure(ex) =>
38 | printErrorTaskMessage("=> EXCEPTION Disjunction <=", ex)
39 | onException(ex)
40 | case Success(Right(value)) => onResult(value)
41 | case Success(Left(ex)) =>
42 | printErrorTaskMessage(s"=> EXCEPTION Xor Left) <=", ex)
43 | onException(ex)
44 | }
45 | }
46 | }
47 |
48 | def resolveAsyncService[E >: Throwable](
49 | onResult: (A) => TaskService[A] = a => TaskService(Task(Either.right(a))),
50 | onException: (E) => TaskService[A] = (e: ServiceException) => TaskService(Task(Either.left(e)))): Unit = {
51 | Task.fork(t.value).runAsync { result =>
52 | result match {
53 | case Failure(ex) =>
54 | printErrorTaskMessage("=> EXCEPTION Disjunction <=", ex)
55 | onException(ex).value.runAsync
56 | case Success(Right(response)) => onResult(response).value.coeval
57 | case Success(Left(ex)) =>
58 | printErrorTaskMessage(s"=> EXCEPTION Xor Left) <=", ex)
59 | onException(ex).value.runAsync
60 | }
61 | }
62 | }
63 |
64 | def resolve[E >: Throwable](
65 | onResult: A => Unit = a => (),
66 | onException: E => Unit = (e: Throwable) => ()): Unit = {
67 | t.value.map {
68 | case Right(response) => onResult(response)
69 | case Left(ex) =>
70 | printErrorTaskMessage("=> EXCEPTION Xor Left <=", ex)
71 | onException(ex)
72 | }.coeval.runAttempt
73 | }
74 |
75 | def resolveService[E >: Throwable](
76 | onResult: (A) => TaskService[A] = a => TaskService(Task(Either.right(a))),
77 | onException: (E) => TaskService[A] = (e: ServiceException) => TaskService(Task(Either.left(e)))): Unit = {
78 | Task.fork(t.value).map {
79 | case Right(response) => onResult(response).value.coeval.runAttempt
80 | case Left(ex) =>
81 | printErrorTaskMessage("=> EXCEPTION Xor Left <=", ex)
82 | onException(ex).value.coeval.runAttempt
83 | }.coeval.runAttempt
84 | }
85 |
86 | def resolveServiceOr[E >: Throwable](exception: (E) => TaskService[A]) = resolveService(onException = exception)
87 |
88 | def resolveAsyncServiceOr[E >: Throwable](exception: (E) => TaskService[A]) = resolveAsyncService(onException = exception)
89 |
90 | }
91 |
92 | }
--------------------------------------------------------------------------------
/modules/commons/src/main/scala/commons/package.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import cats.data._
18 | import cats.{Functor, Monad}
19 | import monix.eval.Task
20 |
21 | import scala.language.{higherKinds, implicitConversions}
22 |
23 | package object commons {
24 |
25 | object TaskService {
26 |
27 | implicit val taskFunctor = new Functor[Task] {
28 | override def map[A, B](fa: Task[A])(f: (A) => B): Task[B] = fa.map(f)
29 | }
30 |
31 | implicit val taskMonad = new Monad[Task] {
32 | override def flatMap[A, B](fa: Task[A])(f: (A) => Task[B]): Task[B] = fa.flatMap(f)
33 | override def pure[A](x: A): Task[A] = Task(x)
34 | override def tailRecM[A, B](a: A)(f: (A) => Task[Either[A, B]]): Task[B] = defaultTailRecM(a)(f)
35 | }
36 |
37 | type TaskService[A] = EitherT[Task, ServiceException, A]
38 |
39 | trait ServiceException extends RuntimeException {
40 | def message: String
41 | def cause: Option[Throwable]
42 | }
43 |
44 | def apply[A](f: Task[ServiceException Either A]) : TaskService[A] = {
45 | EitherT[Task, ServiceException, A](f)
46 | }
47 |
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/modules/services/src/main/scala/com/fortysevendeg/architecture/services/api/ApiService.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.services.api
18 |
19 | import commons.TaskService._
20 |
21 | trait ApiService {
22 |
23 | def getAnimals(simulateFail: Boolean): TaskService[Seq[Animal]]
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/modules/services/src/main/scala/com/fortysevendeg/architecture/services/api/Exceptions.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.services.api
18 |
19 | import commons.TaskService.ServiceException
20 |
21 | case class ApiServiceException(message: String, cause: Option[Throwable] = None)
22 | extends RuntimeException(message)
23 | with ServiceException {
24 | cause map initCause
25 | }
26 |
27 | trait ImplicitsApiServiceExceptions {
28 | implicit def apiServiceExceptionConverter = (t: Throwable) => ApiServiceException(t.getMessage, Option(t))
29 | }
30 |
--------------------------------------------------------------------------------
/modules/services/src/main/scala/com/fortysevendeg/architecture/services/api/Models.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.services.api
18 |
19 | case class Animal(name: String, url: String)
20 |
--------------------------------------------------------------------------------
/modules/services/src/main/scala/com/fortysevendeg/architecture/services/api/impl/ApiServiceImpl.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.fortysevendeg.architecture.services.api.impl
18 |
19 | import com.fortysevendeg.architecture.services.api.{Animal, ApiService, ApiServiceException, ImplicitsApiServiceExceptions}
20 | import commons._
21 | import commons.TaskService._
22 | import monix.eval.Task
23 |
24 | class ApiServiceImpl
25 | extends ApiService
26 | with ImplicitsApiServiceExceptions {
27 |
28 | override def getAnimals(simulateFail: Boolean = false): TaskService[Seq[Animal]] =
29 | TaskService {
30 | Task {
31 | CatchAll[ApiServiceException] {
32 | Thread.sleep(1500)
33 | if (simulateFail) throw new RuntimeException
34 | 1 to 10 map { i =>
35 | Animal(s"Item $i", s"http://lorempixel.com/500/500/animals/$i")
36 | }
37 | }
38 | }
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/project/AppBuild.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import sbt._
18 | import Settings._
19 |
20 | object AppBuild extends Build {
21 |
22 | lazy val androidScala = Project(
23 | id = "android",
24 | base = file("modules/android"),
25 | settings = android.Plugin.androidBuild ++ androidAppSettings
26 | ).dependsOn(services, commons)
27 |
28 | lazy val services = Project(id = "services", base = file("modules/services"))
29 | .settings(servicesSettings).dependsOn(commons)
30 |
31 | lazy val commons = Project(id = "commons", base = file("modules/commons"))
32 | .settings(sarchSettings)
33 |
34 | }
--------------------------------------------------------------------------------
/project/Libraries.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import sbt._
18 |
19 | object Libraries {
20 |
21 | def onCompile(dep: ModuleID): ModuleID = dep % "compile"
22 | def onTest(dep: ModuleID): ModuleID = dep % "test"
23 |
24 | object scala {
25 |
26 | lazy val scalaReflect = "org.scala-lang" % "scala-reflect" % Versions.scalaV
27 | lazy val scalap = "org.scala-lang" % "scalap" % Versions.scalaV
28 | }
29 |
30 | object monix {
31 | lazy val monixTypes = "io.monix" %% "monix-types" % Versions.monixV
32 | lazy val monixEval = "io.monix" %% "monix-eval" % Versions.monixV
33 | }
34 |
35 | object cats {
36 | lazy val cats = "org.typelevel" %% "cats-core" % Versions.catsV
37 | }
38 |
39 | object android {
40 |
41 | def androidDep(module: String) = "com.android.support" % module % Versions.androidV
42 |
43 | lazy val androidSupportv4 = androidDep("support-v4")
44 | lazy val androidAppCompat = androidDep("appcompat-v7")
45 | lazy val androidRecyclerview = androidDep("recyclerview-v7")
46 | lazy val androidCardView = androidDep("cardview-v7")
47 | lazy val androidDesign = androidDep("design")
48 | }
49 |
50 | object playServices {
51 |
52 | def playServicesDep(module: String) = "com.google.android.gms" % module % Versions.playServicesV
53 |
54 | lazy val playServicesGooglePlus = playServicesDep("play-services-plus")
55 | lazy val playServicesAccountLogin = playServicesDep("play-services-identity")
56 | lazy val playServicesActivityRecognition = playServicesDep("play-services-location")
57 | lazy val playServicesAppIndexing = playServicesDep("play-services-appindexing")
58 | lazy val playServicesCast = playServicesDep("play-services-cast")
59 | lazy val playServicesDrive = playServicesDep("play-services-drive")
60 | lazy val playServicesFit = playServicesDep("play-services-fitness")
61 | lazy val playServicesMaps = playServicesDep("play-services-maps")
62 | lazy val playServicesAds = playServicesDep("play-services-ads")
63 | lazy val playServicesPanoramaViewer = playServicesDep("play-services-panorama")
64 | lazy val playServicesGames = playServicesDep("play-services-games")
65 | lazy val playServicesWallet = playServicesDep("play-services-wallet")
66 | lazy val playServicesWear = playServicesDep("play-services-wearable")
67 | // Google Actions, Google Analytics and Google Cloud Messaging
68 | lazy val playServicesBase = playServicesDep("play-services-base")
69 | }
70 |
71 | object graphics {
72 | lazy val picasso = "com.squareup.picasso" % "picasso" % Versions.picassoV
73 | }
74 |
75 | object akka {
76 |
77 | def akka(module: String) = "com.typesafe.akka" %% s"akka-$module" % Versions.akkaV
78 |
79 | lazy val akkaActor = akka("actor")
80 | lazy val akkaTestKit = akka("testkit")
81 |
82 | }
83 |
84 | object macroid {
85 |
86 | def macroid(module: String = "") =
87 | "org.macroid" %% s"macroid${if(!module.isEmpty) s"-$module" else ""}" % Versions.macroidV
88 |
89 | lazy val macroidRoot = macroid()
90 | lazy val macroidAkkaFragments = macroid("akka")
91 | lazy val macroidExtras = "com.fortysevendeg" %% "macroid-extras" % Versions.macroidExtrasV
92 | }
93 |
94 | object json {
95 | lazy val playJson = "com.typesafe.play" %% "play-json" % Versions.playJsonV
96 | }
97 |
98 | object net {
99 | lazy val communicator = "io.taig" %% "communicator" % Versions.communicatorV
100 |
101 | }
102 |
103 | object test {
104 | lazy val specs2 = "org.specs2" %% "specs2-core" % Versions.specs2V % "test"
105 | lazy val androidTest = "com.google.android" % "android" % "4.1.1.4" % "test"
106 | lazy val mockito = "org.specs2" % "specs2-mock_2.11" % Versions.mockitoV % "test"
107 | }
108 | }
--------------------------------------------------------------------------------
/project/Settings.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import Libraries.android._
18 | import Libraries.cats._
19 | import Libraries.json._
20 | import Libraries.macroid._
21 | import Libraries.monix._
22 | import Libraries.test._
23 | import Libraries.graphics._
24 | import android.Keys._
25 | import sbt.Keys._
26 | import sbt._
27 |
28 | object Settings {
29 |
30 | // Android Module
31 | lazy val androidAppSettings = basicSettings ++
32 | Seq(
33 | name := "scala-android-architecture",
34 | platformTarget in Android := "android-23",
35 | run <<= run in Android,
36 | ivyScala := ivyScala.value map {
37 | _.copy(overrideScalaVersion = true)
38 | },
39 | javacOptions in Compile ++= Seq("-target", "1.7", "-source", "1.7"),
40 | transitiveAndroidLibs in Android := true,
41 | libraryDependencies ++= androidDependencies,
42 | packagingOptions in Android := PackagingOptions(
43 | Seq("META-INF/LICENSE",
44 | "META-INF/LICENSE.txt",
45 | "META-INF/NOTICE",
46 | "META-INF/NOTICE.txt")),
47 | dexMaxHeap in Android := "2048m",
48 | proguardScala in Android := true,
49 | useProguard in Android := true,
50 | proguardCache in Android := Seq.empty,
51 | proguardOptions in Android ++= proguardCommons,
52 | dexMulti in Android := true)
53 |
54 | // Services Module
55 | lazy val servicesSettings = basicSettings ++ librarySettings
56 |
57 | // Process Module
58 | lazy val jobsSettings = basicSettings ++ librarySettings
59 |
60 | // Sarch Module
61 | lazy val sarchSettings = basicSettings ++ librarySettings
62 |
63 | lazy val androidDependencies = Seq(
64 | aar(macroidRoot),
65 | aar(androidSupportv4),
66 | aar(androidAppCompat),
67 | aar(androidDesign),
68 | aar(androidCardView),
69 | aar(androidRecyclerview),
70 | aar(macroidExtras),
71 | playJson,
72 | picasso,
73 | specs2,
74 | mockito,
75 | androidTest)
76 |
77 | // Basic Setting for all modules
78 | lazy val basicSettings = Seq(
79 | scalaVersion := Versions.scalaV,
80 | resolvers ++= commonResolvers,
81 | libraryDependencies ++= Seq(cats, monixTypes, monixEval)
82 | )
83 |
84 | lazy val duplicatedFiles = Set("AndroidManifest.xml")
85 |
86 | // Settings associated to library modules
87 | lazy val librarySettings = Seq(
88 | mappings in(Compile, packageBin) ~= {
89 | _.filter { tuple =>
90 | !duplicatedFiles.contains(tuple._1.getName)
91 | }
92 | },
93 | exportJars := true,
94 | scalacOptions in Compile ++= Seq("-deprecation", "-Xexperimental"),
95 | javacOptions in Compile ++= Seq("-target", "1.7", "-source", "1.7"),
96 | javacOptions in Compile += "-deprecation",
97 | proguardScala in Android := false)
98 |
99 | lazy val commonResolvers =
100 | Seq(
101 | Resolver.mavenLocal,
102 | DefaultMavenRepository,
103 | Resolver.typesafeRepo("releases"),
104 | Resolver.typesafeRepo("snapshots"),
105 | Resolver.typesafeIvyRepo("snapshots"),
106 | Resolver.sonatypeRepo("releases"),
107 | Resolver.sonatypeRepo("snapshots"),
108 | Resolver.defaultLocal,
109 | Resolver.jcenterRepo,
110 | "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases"
111 | )
112 |
113 | lazy val proguardCommons = Seq(
114 | "-ignorewarnings",
115 | "-keep class scala.Dynamic",
116 | "-keep class com.fortysevendeg.scala.android.** { *; }",
117 | "-keep class macroid.** { *; }",
118 | "-keep class android.** { *; }")
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/project/Versions.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | * not use this file except in compliance with the License. You may obtain
6 | * a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | object Versions {
18 |
19 | val appV = "1.0.0"
20 | val scalaV = "2.11.7"
21 | val androidPlatformV = "android-23"
22 | val androidV = "23.3.0"
23 | val macroidExtrasV = "0.3"
24 | val macroidV = "2.0.0-M5"
25 | val akkaV = "2.3.6"
26 | val playServicesV = "9.2.0"
27 | val playJsonV = "2.3.4"
28 | val picassoV = "2.5.0"
29 | val specs2V = "3.6.1"
30 | val mockitoV = "3.6.1"
31 | val communicatorV = "2.0.0"
32 | val monixV = "2.0.0"
33 | val catsV = "0.7.2"
34 | }
35 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (C) 2016 47 Degrees, LLC http://47deg.com hello@47deg.com
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may
5 | # not use this file except in compliance with the License. You may obtain
6 | # a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | #
16 |
17 | sbt.version=0.13.8
18 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | logLevel := Level.Info
2 | addSbtPlugin("com.hanhuy.sbt" % "android-sdk-plugin" % "1.5.19")
3 |
--------------------------------------------------------------------------------
/resources/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/47degrees/scala-android-architecture/c4452b1957f5cee067b1bbcf221489dce5efa31d/resources/architecture.png
--------------------------------------------------------------------------------