TestDispatcherの調査

This commit is contained in:
tateisu 2023-01-15 14:04:37 +09:00
parent 60bb5f7e7a
commit b2b47e730a
24 changed files with 407 additions and 253 deletions

View File

@ -1,5 +1,5 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: "java-library"
apply plugin: "kotlin"
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
@ -18,7 +18,7 @@ compileKotlin {
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection DifferentStdlibGradleVersion
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
testImplementation "junit:junit:$junit_version"

View File

@ -1,5 +1,5 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
compileSdkVersion compile_sdk_version
@ -20,7 +20,7 @@ android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
@ -41,6 +41,6 @@ repositories {
}
dependencies {
api project(':apng')
api project(":apng")
implementation project(":base")
}

View File

@ -11,7 +11,6 @@ apply plugin: "io.gitlab.arturbosch.detekt"
android {
compileSdkVersion compile_sdk_version
buildToolsVersion build_tools_version

View File

@ -23,6 +23,7 @@ import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.showToast
import jp.juggler.util.network.toFormRequestBody
import jp.juggler.util.network.toPost
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private val log = LogCategory("Action_Tag")

View File

@ -43,6 +43,7 @@ import jp.juggler.util.log.showToast
import jp.juggler.util.ui.activity
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.createColoredDrawable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.anko.backgroundColor
import java.lang.ref.WeakReference

View File

@ -84,7 +84,7 @@ class PollingWorker2(
workManager.enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
ExistingPeriodicWorkPolicy.REPLACE,
workRequest
).await()

View File

@ -29,6 +29,7 @@ import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.network.toPut
import jp.juggler.util.network.toPutRequestBuilder
import jp.juggler.util.ui.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay

View File

@ -21,6 +21,7 @@ import jp.juggler.util.network.MEDIA_TYPE_JSON
import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.ui.*
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import okhttp3.Request

View File

@ -113,16 +113,16 @@
<color name="Mastodon_colorConversationMainTootBg">#2000a2ff</color>
<color name="Mastodon_colorLink">#FE4E92D6</color>
<color name="Mastodon_colorListItemDrag">#AA444444</color>
<color name="Mastodon_colorListItemDrag">#AA2F6091</color>
<color name="Mastodon_colorPostFormBackground">#222</color>
<color name="Mastodon_colorActionBarBg">#333</color>
<color name="Mastodon_colorActionBarBgStacked">#222</color>
<color name="Mastodon_colorActionBarBg">#444B5D</color>
<color name="Mastodon_colorActionBarBgStacked">#444B5D</color>
<color name="Mastodon_colorStatusBarBg">#444B5D</color>
<color name="Mastodon_colorProfileBackgroundMask">#C0000000</color>
<color name="Mastodon_colorRefreshErrorBg">#D222</color>
<color name="Mastodon_colorRegexFilterError">#f00</color>
<color name="Mastodon_colorReplyBackground">#333</color>
<color name="Mastodon_colorRippleEffect">#777</color>
<color name="Mastodon_colorRippleEffect">#C1607ECC</color>
<color name="Mastodon_colorSearchFormBackground">#333370</color>
<color name="Mastodon_colorSettingDivider">#66FFFFFF</color><!-- ダイアログ背景が#424242なので、それより明るくないといけない -->
<color name="Mastodon_colorShowMediaBackground">#222</color>

View File

@ -1,17 +1,17 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id "com.android.library"
id "org.jetbrains.kotlin.android"
// apply plugin: 'kotlin-android'
// apply plugin: 'kotlin-kapt'
// apply plugin: "kotlin-android"
// apply plugin: "kotlin-kapt"
// import java.text.SimpleDateFormat
// apply plugin: 'com.android.application'
// apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
// apply plugin: 'com.google.gms.google-services'
// apply plugin: "com.android.application"
// apply plugin: "org.jetbrains.kotlin.plugin.serialization"
// apply plugin: "com.google.gms.google-services"
}
android {
namespace 'jp.juggler.base'
namespace "jp.juggler.base"
compileSdk compile_sdk_version
defaultConfig {
@ -25,7 +25,7 @@ android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
@ -36,7 +36,7 @@ android {
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = "1.8"
}
}
@ -46,132 +46,52 @@ dependencies {
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_lib_bersion"
def emoji2Version = "1.2.0"
api "androidx.emoji2:emoji2:$emoji2Version"
api "androidx.emoji2:emoji2-views:$emoji2Version"
api "androidx.emoji2:emoji2-views-helper:$emoji2Version"
api "androidx.emoji2:emoji2-bundled:$emoji2Version"
api "androidx.appcompat:appcompat:$appcompat_version"
api 'androidx.core:core-ktx:1.9.0'
api 'com.google.android.material:material:1.7.0'
api "androidx.core:core-ktx:1.9.0"
// DrawerLayout
api "androidx.drawerlayout:drawerlayout:1.1.1"
// NavigationView
api "com.google.android.material:material:1.7.0"
// CustomTabs
api "androidx.browser:browser:1.4.0"
// Recyclerview
api "androidx.recyclerview:recyclerview:1.2.1"
api "androidx.exifinterface:exifinterface:1.3.5"
api "androidx.core:core-ktx:1.9.0"
api "androidx.drawerlayout:drawerlayout:1.1.1"
api "androidx.emoji2:emoji2-bundled:$emoji2Version"
api "androidx.emoji2:emoji2-views-helper:$emoji2Version"
api "androidx.emoji2:emoji2-views:$emoji2Version"
api "androidx.emoji2:emoji2:$emoji2Version"
api "androidx.exifinterface:exifinterface:1.3.5"
api "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
api "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
api "androidx.lifecycle:lifecycle-process:$lifecycle_version"
api "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"
api "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
api "androidx.lifecycle:lifecycle-service:$lifecycle_version"
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
api "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
api "androidx.recyclerview:recyclerview:1.2.1"
api "androidx.room:room-ktx:$roomVersion"
api "androidx.room:room-runtime:$roomVersion"
api "androidx.startup:startup-runtime:$startup_version"
api "androidx.work:work-runtime-ktx:$workVersion"
api "androidx.work:work-runtime:$workVersion"
api "com.astuetz:pagerslidingtabstrip:1.0.1"
api "com.caverock:androidsvg-aar:1.4"
api "com.github.hadilq:live-event:1.3.0"
api "com.github.kenglxn.QRGen:android:2.5.0"
api "com.github.omadahealth:swipy:1.2.3@aar"
api "com.github.woxthebox:draglistview:1.6.6"
api "com.google.android.exoplayer:exoplayer:2.18.2"
api "com.google.android.flexbox:flexbox:3.0.0"
api "com.google.android.material:material:1.7.0"
api "com.google.firebase:firebase-messaging:23.1.1"
api "com.otaliastudios:transcoder:0.10.4"
api "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion"
api "com.squareup.okhttp3:okhttp:$okhttpVersion"
api "io.github.inflationx:calligraphy3:3.1.1"
api "io.github.inflationx:viewpump:2.0.3"
api "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$kotlinx_coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
api "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// ViewModel
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
api "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Saved state module for ViewModel
api "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
// if using Java8, use the following instead of lifecycle-compiler
api "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// optional - helpers for implementing LifecycleOwner in a Service
api "androidx.lifecycle:lifecycle-service:$lifecycle_version"
// optional - ProcessLifecycleOwner provides a lifecycle for the whole application process
api "androidx.lifecycle:lifecycle-process:$lifecycle_version"
// optional - ReactiveStreams support for LiveData
api "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"
api "androidx.startup:startup-runtime:$startup_version"
// video transcoder https://github.com/natario1/Transcoder
api "com.otaliastudios:transcoder:0.10.4"
api 'io.github.inflationx:calligraphy3:3.1.1'
api 'io.github.inflationx:viewpump:2.0.3'
api "com.squareup.okhttp3:okhttp:$okhttpVersion"
api "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion"
api "ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0"
api 'com.github.woxthebox:draglistview:1.6.6'
api 'com.github.omadahealth:swipy:1.2.3@aar'
api 'com.github.kenglxn.QRGen:android:2.5.0'
api "com.google.android.flexbox:flexbox:3.0.0"
api 'com.astuetz:pagerslidingtabstrip:1.0.1'
api 'com.caverock:androidsvg-aar:1.4'
api "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// ViewModel
api "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
//noinspection KtxExtensionAvailable
//api "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
// LiveData
api "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Saved state module for ViewModel
api "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"
// if using Java8, use the following instead of lifecycle-compiler
api "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
// optional - helpers for implementing LifecycleOwner in a Service
api "androidx.lifecycle:lifecycle-service:$lifecycle_version"
// optional - ProcessLifecycleOwner provides a lifecycle for the whole application process
api "androidx.lifecycle:lifecycle-process:$lifecycle_version"
// optional - ReactiveStreams support for LiveData
api "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version"
api 'androidx.work:work-runtime-ktx:2.8.0-rc01'
api "androidx.room:room-runtime:$roomVersion"
api "androidx.room:room-ktx:$roomVersion"
api 'com.google.android.exoplayer:exoplayer:2.18.2'
/*
WARNING: [Processor] Library '…\exoplayer-ui-2.12.0.aar' contains references to both AndroidX and old support library. This seems like the library is partially migrated. Jetifier will try to rewrite the library anyway.
Example of androidX reference: 'androidx/core/app/NotificationCompat$Builder'
Example of support library reference: 'android/support/v4/media/session/MediaSessionCompat$Token'
expPlayerも苦労してるんだなあ
*/
// LiveEvent
api "com.github.hadilq:live-event:1.3.0"
api "androidx.work:work-runtime:$workVersion"
api "androidx.work:work-runtime-ktx:$workVersion"
api "androidx.startup:startup-runtime:$startup_version"
// Koin main features for Android
api "io.insert-koin:koin-android:$koin_version"
api "io.insert-koin:koin-android-compat:$koin_version"
@ -179,54 +99,35 @@ dependencies {
// api "io.insert-koin:koin-androidx-navigation:$koin_version"
// api "io.insert-koin:koin-androidx-compose:$koin_version"
// https://firebase.google.com/support/release-notes/android
api "com.google.firebase:firebase-messaging:23.1.1"
api "com.github.bumptech.glide:glide:$glideVersion"
api "com.github.bumptech.glide:annotations:$glideVersion"
api("com.github.bumptech.glide:okhttp3-integration:$glideVersion") {
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
exclude group: "com.squareup.okhttp3", module: "okhttp"
}
// kotlin-testとjunitを併用
testApi "androidx.arch.core:core-testing:$arch_version"
testApi "junit:junit:$junit_version"
testApi "org.jetbrains.kotlin:kotlin-test"
testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
// optional - Test helpers for LiveData
testApi "androidx.arch.core:core-testing:$arch_version"
// optional - Test helpers for LiveData
testApi "androidx.arch.core:core-testing:$arch_version"
androidTestApi "androidx.test.espresso:espresso-core:3.5.1"
androidTestApi "androidx.test.ext:junit:1.1.5"
androidTestApi "androidx.test:core:$androidx_test_version"
androidTestApi "org.jetbrains.kotlin:kotlin-test"
androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version"
testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
exclude group: 'com.squareup.okio', module: 'okio'
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
exclude group: "com.squareup.okio", module: "okio"
exclude group: "com.squareup.okhttp3", module: "okhttp"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-common"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8"
}
androidTestApi 'androidx.test.ext:junit:1.1.5'
// targetSdkVersion 31 androidTest android:exported
// https://github.com/android/android-test/issues/1022
androidTestApi "androidx.test:core:$androidx_test_version"
androidTestApi 'androidx.test.espresso:espresso-core:3.5.1'
// androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0-alpha4', {
// exclude group: 'com.android.support', module: 'support-annotations'
// })
// androidTestApi('androidx.test.espresso:espresso-core:3.1.0-alpha4', {
// exclude group: 'com.android.support', module: 'support-annotations'
// })
androidTestApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") {
exclude group: 'com.squareup.okio', module: 'okio'
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
exclude group: "com.squareup.okio", module: "okio"
exclude group: "com.squareup.okhttp3", module: "okhttp"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-common"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib"
exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8"
}
}

View File

@ -0,0 +1,165 @@
package jp.juggler.base
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.test.ext.junit.runners.AndroidJUnit4
import jp.juggler.util.coroutine.AppDispatchers
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.*
import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.atomic.AtomicBoolean
/**
* kotlinx.coroutines.test の使い方の説明
* https://developer.android.com/kotlin/coroutines/test?hl=ja#testdispatchers
*/
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class DispatchersTest {
// 単純なリポジトリ
private class UserRepository {
val names = ArrayList<String>()
fun register(name: String) = names.add(name)
fun getAllUsers(): List<String> = names
}
// Dispatcherを受け取るリポジトリ
private class Repository(
private val ioDispatcher: CoroutineDispatcher = AppDispatchers.io,
) {
private val ioScope = CoroutineScope(ioDispatcher)
val initialized = AtomicBoolean(false)
// A function that starts a new coroutine on the IO dispatcher
fun initializeAsync() = ioScope.async {
delay(100L)
initialized.set(true)
}
// A suspending function that switches to the IO dispatcher
suspend fun fetchData(): String = withContext(ioDispatcher) {
require(initialized.get()) { "Repository should be initialized first" }
delay(500L)
"Hello world"
}
}
//================================================================
// テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる
// プロパティは記述順に初期化されることに注意
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
// スケジューラを共有するリポジトリ
private val repository = Repository(mainDispatcherRule.testDispatcher)
//====================================================
// テストでの suspend 関数の呼び出し
// runTestを使う
private suspend fun fetchData(): String {
delay(1000L)
return "Hello world"
}
@Test
fun useRunTest() = runTest {
assertEquals("Hello world", fetchData())
}
//====================================================
// launch内部の処理を待つテストコード
@Test
fun useAdvanceUntilIdle() = runTest {
val userRepo = UserRepository()
launch { userRepo.register("Alice") }
launch { userRepo.register("Bob") }
advanceUntilIdle() // Yields to perform the registrations
assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
}
//==============================================
// UnconfinedTestDispatcher を使うとlaunch内部が先に実行開始する
// ただしlaunch内部で非同期待機が入ると外側の実行が再開される
@Test
fun useUnconfinedTestDispatcher() = runTest(UnconfinedTestDispatcher()) {
val userRepo = UserRepository()
launch { userRepo.register("Alice") }
launch { userRepo.register("Bob") }
assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes
}
// =============================================
// viewModelScopeなどが使うディスパッチャーを差し替える
class HomeViewModel : ViewModel() {
private val _message = MutableStateFlow("")
val message: StateFlow<String> get() = _message
fun loadMessage() {
viewModelScope.launch {
_message.value = "Greetings!"
}
}
}
@Test
fun useDispatchersSetMain() = runTest {
// MainDispatcherRule を指定しているので、viewModelが使う Dispatcher が変わる
val viewModel = HomeViewModel()
viewModel.loadMessage()
assertEquals("Greetings!", viewModel.message.value)
}
// =============================================================
// リポジトリクラスにDispatcherを渡せるようにする
@Test
fun useRepoWithTestDispatcher() = runTest {
val repository = Repository(
ioDispatcher = StandardTestDispatcher(testScheduler)
)
repository.initializeAsync().await()
assertEquals(true, repository.initialized.get())
assertEquals("Hello world", repository.fetchData())
}
//=======================================================
// プロパティ間でスケジューラを共有する
@Test
fun someRepositoryTest() = runTest {
// Takes scheduler from Main
// Any TestDispatcher created here also takes the scheduler from Main
// val newTestDispatcher = StandardTestDispatcher()
// これもStandardTestDispatcher を作成する
// 注意: 独自の TestScope を作成する場合は、テスト内のそのスコープで runTest を呼び出す必要があります。
// テストには TestScope インスタンスを 1 つだけ含めることができます。
// val testScope = TestScope()
repository.initializeAsync().await()
assertEquals(true, repository.initialized.get())
assertEquals("Hello world", repository.fetchData())
}
//=======================================================
// DI
// クラス内に以下のようなプロパティを定義しておくこともできる。
// DIする際は参考になるかもしれない。
// val testScheduler = TestCoroutineScheduler()
// val testDispatcher = StandardTestDispatcher(testScheduler)
// val testScope = TestScope(testDispatcher)
//
// fun xxx() = testScope.runTest{ ... }
}

View File

@ -2,6 +2,9 @@ package jp.juggler.base
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import jp.juggler.base.JugglerBase.Companion.jugglerBase
import jp.juggler.base.JugglerBase.Companion.jugglerBaseNullable
import jp.juggler.base.JugglerBase.Companion.prepareJugglerBase
import org.junit.Test
import org.junit.runner.RunWith
@ -14,11 +17,12 @@ import org.junit.Assert.*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
class JugglerBaseTest {
@Test
fun useAppContext() {
// Context of the app under test.
fun initializeJubblerBase() {
assertNotNull("JubblerBase is initialized for a test.", jugglerBaseNullable)
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("jp.juggler.base.test", appContext.packageName)
appContext.prepareJugglerBase
assertNotNull( "JubblerBase is initialized after prepare.",jugglerBase)
}
}
}

View File

@ -0,0 +1,32 @@
package jp.juggler.base
import jp.juggler.util.coroutine.AppDispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
/**
* Dispatchers.Main のテスト中の置き換えを複数テストで衝突しないようにルール化する
* https://developer.android.com/kotlin/coroutines/test?hl=ja
*/
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
/**
* UnconfinedTestDispatcher StandardTestDispatcher のどちらかを指定する
*/
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
AppDispatchers.setTest(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}

View File

@ -0,0 +1,50 @@
package jp.juggler.base
import android.annotation.SuppressLint
import android.content.Context
import androidx.startup.AppInitializer
import androidx.startup.Initializer
import jp.juggler.util.log.LogCategory
/**
* AndroidManifest.xml の指定により
* ApplicationのonCreate()より前に実行される
*/
class JugglerBaseInitializer : Initializer<JugglerBase> {
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
override fun create(context: Context) =
JugglerBase(context.applicationContext)
}
/**
*
*/
class JugglerBase(
var context: Context,
) {
companion object {
private val log = LogCategory("JugglerBase")
/**
* 最後に作成したインスタンス
*/
@SuppressLint("StaticFieldLeak")
var jugglerBaseNullable: JugglerBase? = null
val jugglerBase get() = jugglerBaseNullable!!
/**
* JugglerBaseのインスタンスを androidx.startup.AppInitializer から取得する
* 遅延初期化を行う場合Contextが必要になる
*/
val Context.prepareJugglerBase: JugglerBase
get() = jugglerBaseNullable
?: AppInitializer.getInstance(applicationContext)
.initializeComponent(JugglerBaseInitializer::class.java)
}
init {
jugglerBaseNullable = this
log.i("ctor")
}
}

View File

@ -1,18 +0,0 @@
package jp.juggler.base
import android.content.Context
import androidx.startup.Initializer
import jp.juggler.util.log.LogCategory
class JugglerBaseInitializer : Initializer<Boolean> {
companion object {
private val log = LogCategory("JugglerBaseInitializer")
}
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
override fun create(context: Context): Boolean {
log.i("create")
return true
}
}

View File

@ -3,10 +3,34 @@ package jp.juggler.util.coroutine
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
/**
* Test時にdispatcherを差し替えられるようにする
*
* https://developer.android.com/kotlin/coroutines/test?hl=ja#testdispatchers
* - TestDispatcher やrunTest を使う
* - Dispatchers.setMain(testDispatcher) Dispatchers.resetMain() でMainを切り替えられる
* - viewModelScope.launch{} などが使うMainを切り替えられる
*
* リポジトリクラスの引数に CoroutineDispatcherを渡すとかもある
*/
object AppDispatchers {
// Main と Main.immediate は Dispatchers.setMain 差し替えられる
val mainImmediate get() = Dispatchers.Main.immediate
var unconfined: CoroutineDispatcher = Dispatchers.Unconfined
var default: CoroutineDispatcher = Dispatchers.Default
var io: CoroutineDispatcher = Dispatchers.IO
var main: CoroutineDispatcher = Dispatchers.Main
var mainImmediate: CoroutineDispatcher = Dispatchers.Main.immediate
fun reset() {
unconfined = Dispatchers.Unconfined
default = Dispatchers.Default
io = Dispatchers.IO
}
fun setTest(testDispatcher: CoroutineDispatcher) {
unconfined = testDispatcher
default = testDispatcher
io = testDispatcher
}
}

View File

@ -3,6 +3,7 @@ package jp.juggler.util.coroutine
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlin.coroutines.CoroutineContext

View File

@ -1,5 +1,3 @@
import io.gitlab.arturbosch.detekt.Detekt
buildscript {
ext.jvm_target = "1.8"
@ -13,7 +11,7 @@ buildscript {
ext.startup_version = "1.1.1"
ext.roomVersion = "2.5.0"
ext.workVersion = "2.7.1"
ext.glideVersion = '4.13.2'
ext.glideVersion = "4.13.2"
ext.appcompat_version = "1.6.0"
ext.lifecycle_version = "2.5.1"
@ -21,20 +19,20 @@ buildscript {
ext.okhttpVersion = "4.10.0"
ext.kotlin_version = '1.7.21'
ext.kotlinx_coroutines_version = '1.6.4'
ext.kotlin_version = "1.7.21"
ext.kotlinx_coroutines_version = "1.6.4"
ext.anko_version = '0.10.8'
ext.anko_version = "0.10.8"
ext.junit_version = '4.13.2'
ext.junit_version = "4.13.2"
ext.detekt_version = '1.22.0'
ext.detekt_version = "1.22.0"
ext.compose_version = '1.0.5'
ext.compose_version = "1.0.5"
ext.koin_version = '3.1.3'
ext.koin_version = "3.1.3"
ext.androidx_test_version = '1.5.0'
ext.androidx_test_version = "1.5.0"
repositories {
google()
@ -42,10 +40,10 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "com.android.tools.build:gradle:7.3.1"
// room google-services
classpath 'com.google.gms:google-services:4.3.14'
classpath "com.google.gms:google-services:4.3.14"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
@ -59,13 +57,13 @@ buildscript {
allprojects {
repositories {
google()
maven { url 'https://maven.google.com' }
maven { url 'https://jitpack.io' }
maven { url "https://maven.google.com" }
maven { url "https://jitpack.io" }
mavenCentral()
maven { url 'https://dl.bintray.com/google/exoplayer/' }
maven { url 'https://dl.bintray.com/google/flexbox-layout/' }
maven { url "https://dl.bintray.com/google/exoplayer/" }
maven { url "https://dl.bintray.com/google/flexbox-layout/" }
}
}
@ -73,7 +71,7 @@ task clean(type: Delete) {
delete rootProject.buildDir
}
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:unchecked'
options.compilerArgs << '-Xlint:deprecation'
options.compilerArgs << '-Xlint:divzero'
options.compilerArgs << "-Xlint:unchecked"
options.compilerArgs << "-Xlint:deprecation"
options.compilerArgs << "-Xlint:divzero"
}

View File

@ -1,5 +1,5 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
compileSdkVersion compile_sdk_version

View File

@ -1,5 +1,5 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
compileSdkVersion compile_sdk_version
@ -19,7 +19,7 @@ android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}

View File

@ -1,10 +1,10 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id "com.android.library"
id "org.jetbrains.kotlin.android"
}
android {
namespace 'jp.juggler.icon_material_symbols'
namespace "jp.juggler.icon_material_symbols"
compileSdk compile_sdk_version
defaultConfig {
minSdk min_sdk_version
@ -16,7 +16,7 @@ android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
compileOptions {
@ -24,6 +24,6 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = "1.8"
}
}

View File

@ -1,5 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: "com.android.application"
apply plugin: "kotlin-android"
android {
compileSdkVersion compile_sdk_version
@ -25,7 +25,7 @@ android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
@ -49,5 +49,5 @@ android {
dependencies {
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_lib_bersion"
implementation project(":base")
implementation project(':apng_android')
implementation project(":apng_android")
}

View File

@ -36,7 +36,7 @@ class ActList : AppCompatActivity(), CoroutineScope {
private lateinit var activityJob: Job
override val coroutineContext: CoroutineContext
get() = AppDispatchers.mainImmediate + activityJob
get() = activityJob + AppDispatchers.mainImmediate
override fun onCreate(savedInstanceState: Bundle?) {
@ -86,7 +86,7 @@ class ActList : AppCompatActivity(), CoroutineScope {
}
private fun load() = launch {
val list = withContext(Dispatchers.IO) {
val list = withContext(AppDispatchers.io) {
// RawリソースのIDと名前の一覧
R.raw::class.java.fields
.mapNotNull { it.get(null) as? Int }
@ -179,7 +179,7 @@ class ActList : AppCompatActivity(), CoroutineScope {
try {
lastJob?.cancelAndJoin()
val job = async(Dispatchers.IO) {
val job = async(AppDispatchers.io) {
try {
ApngFrames.parse(128) { resources?.openRawResource(resId) }
} catch (ex: Throwable) {

View File

@ -7,16 +7,17 @@ import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.apng.ApngFrames
import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.coroutine.AsyncActivity
import jp.juggler.util.int
import jp.juggler.util.string
import kotlinx.coroutines.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import kotlin.coroutines.CoroutineContext
class ActViewer : AppCompatActivity(), CoroutineScope {
class ActViewer : AsyncActivity() {
companion object {
const val TAG = "ActViewer"
@ -34,13 +35,7 @@ class ActViewer : AppCompatActivity(), CoroutineScope {
private lateinit var apngView: ApngView
private lateinit var tvError: TextView
private lateinit var activityJob: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + activityJob
override fun onCreate(savedInstanceState: Bundle?) {
activityJob = Job()
super.onCreate(savedInstanceState)
val resId = intent.int(EXTRA_RES_ID) ?: 0
@ -64,7 +59,7 @@ class ActViewer : AppCompatActivity(), CoroutineScope {
launch {
var apngFrames: ApngFrames? = null
try {
apngFrames = withContext(Dispatchers.IO) {
apngFrames = withContext(AppDispatchers.io) {
try {
ApngFrames.parse(
1024,
@ -99,13 +94,12 @@ class ActViewer : AppCompatActivity(), CoroutineScope {
override fun onDestroy() {
super.onDestroy()
apngView.apngFrames?.dispose()
activityJob.cancel()
}
private fun save(apngFrames: ApngFrames) {
val title = this.title
launch(Dispatchers.IO) {
launch(AppDispatchers.io) {
//deprecated in Android 10 (API level 29)
//val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
@ -139,4 +133,4 @@ class ActViewer : AppCompatActivity(), CoroutineScope {
}
}
}
}
}