├── .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 | ![Architecture](resources/architecture.png) 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 | 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 --------------------------------------------------------------------------------