Microsoft MVP성태의 닷넷 이야기
Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어 [링크 복사], [링크+제목 복사],
조회: 9465
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 2개 있습니다.)
(시리즈 글이 4개 있습니다.)
Phone: 16. C# MAUI - /Download 등의 공용 디렉터리에 접근하는 방법
; https://www.sysnet.pe.kr/2/0/13631

Phone: 17. C# MAUI - Android 내에 Web 서비스 호스팅
; https://www.sysnet.pe.kr/2/0/13632

Phone: 18. C# MAUI - 안드로이드 플랫폼에서의 Activity 제어
; https://www.sysnet.pe.kr/2/0/13634

Phone: 21. C# MAUI - Android 환경에서의 파일 다운로드(DownloadManager)
; https://www.sysnet.pe.kr/2/0/13640




C# MAUI - 안드로이드 플랫폼에서의 Activity 제어

(이 글은 안드로이드 환경만을 대상으로 합니다.)

MAUI가 기본적으로 XAML을 사용한 UI를 지원하지만 전통적인 Activity를 사용하는 것도 가능합니다. 예를 하나 들어볼까요? 간단하게 Button Click을 하나 만들고,

<Button
        x:Name="btnNewActivity"
        Text="New Activity" 
        SemanticProperties.Hint="Click to show new Activity"
        Clicked="OnNewActivityClicked"
        HorizontalOptions="Fill" />

이벤트 핸들러에서 이런 식으로 Activity를 띄울 수 있습니다.

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void OnNewActivityClicked(object sender, EventArgs e)
    {
#if ANDROID
        var currentActivity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
        if (currentActivity == null)
        {
            return;
        }

        var newIntent = new Intent(currentActivity, typeof(NewActivity));
        currentActivity.StartActivity(newIntent);
#endif
    }
}

NewActivity 클래스는 .\Platforms\Android 경로에 대충 이렇게 추가해 두면 됩니다.

// NewActivity.cs

using Android.App;
using Android.OS;

namespace SimplePlayer;

[Activity]
internal class NewActivity : Activity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }
}

당연히 이렇게만 하면 버튼을 클릭했을 때 뜨는 NewActivity 화면은 비어 있게 됩니다.




그나저나, 제가 Xamarin Forms부터 이어져온 .NET MAUI에 대한 이력을 전혀 모르고 있습니다. ^^; 게다가 구조도 아직 눈에 안 들어와서 초보 사용자 수준으로만 다뤄보고 있는데요, 그래서 아직 모르는 것인지, 그런 방법이 없는 것인지 알 수 없지만 현재 .NET MAUI는 XAML을 이용한 폼 구성을 MainActivity에만 종속시키고 있는 듯합니다.

다시 말해, 제가 위에서 만든 NewActivity를 위해 XAML로 UI를 구성할 수 없다는 것입니다. (혹시 방법을 아시는 분은 덧글 부탁드립니다. ^^)

이와 관련해서는 검색해도 자료도 없고, 어느 정도 저렇다고 판단할 수밖에 없는 근거가 있다면, 바로 App.Current에 대한 접근이 static 필드라는 사실입니다.

public static Application? Current { get; set; }

여기서 Current는 .NET MAUI 프로젝트의 App.xaml/App.xaml.cs에 해당하는 인스턴스가 설정되는데요, 그렇다면 오직 하나의 XAML Application 구조만 생성된다는 것이고 그것이 MainActivity가 소유한 Window에만 종속돼 있을 것입니다. 이런 상황에서 다른 Activity에서 (이미 MainActivity가 소유한) XAML을 임의로 가져가서 쓴다는 것이 맞지 않습니다. 애초에 이게 가능하려면 AppShell이나 App이 다중으로 생성될 수 있는 구조여야 하고 그것과 Activity가 연결되었다는 정보가 있어야만 합니다.

하지만, .NET MAUI가 생성하는 MainActivity는 딱히 AppShell/MainPage XAML을 소유한다는 식의 정보는 소스코드 내에 어떤 식으로도 명시돼 있지 않습니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    )]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }
}




자, 그래서 우리가 새로 생성한 Activity인 경우에는 전통적인 Android 방식의 UI 시스템을 따라야 합니다. 이를 위해 .\Platforms\Android\Resources\layout 디렉터리에 activity_new.axml 파일을 만들어 다음의 내용으로 채웁니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical" >
    <TextView android:id="@+id/txtOutput"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:text="Hello, world" />
    <Button android:id="@+id/btnSample"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Click" />
</LinearLayout>

그다음 NewActivity에서는 위의 layout을 이렇게 연결하면 됩니다.

[Activity]
internal class NewActivity : Activity
{
    TextView? _txtOutput;
    Android.Widget.Button? _btnSample;

    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        this.SetContentView(Resource.Layout.activity_new);

        _txtOutput = FindViewById<TextView>(Resource.Id.txtOutput);
        _btnSample = FindViewById<Android.Widget.Button>(Resource.Id.btnSample);

        if (_btnSample != null)
        {
            _btnSample.Click += _btnSample_Click;
        }
    }

    private void _btnSample_Click(object? sender, EventArgs e)
    {
        _txtOutput.Text = "Hello, .NET MAUI";
    }
}




그런데 여기서 한 가지 재미있는 점이 있습니다. (XAML과 연결된) MainActivity를 제외하고는, 위와 같이 임의로 생성한 Activity는 MAUI의 Navigation 기능인 Shell.Current.GoToAsync를 사용할 수 없습니다.

일례로, _btnSample_Click에서 다음과 같이 코드를 추가하고,

private async void _btnSample_Click(object? sender, EventArgs e)
{
    _txtOutput.Text = "Hello, .NET MAUI";

    await Shell.Current.GoToAsync($"//{nameof(MainPage)}");
}

실행 후, 버튼을 누르면 MainPage로의 전환이 되지 않습니다. 아래의 질문도 이와 관련된 것인데요,

Navigate to a page in .Net MAUI Anroid Activity
; https://stackoverflow.com/questions/77265785/navigate-to-a-page-in-net-maui-anroid-activity

다시 말해 저렇게 동작할 수는 없습니다. 사실 일면 이해는 됩니다. 위의 구조를 Windows 운영체제로 대입해 보면, WPF Window 내에 Page XAML이 구성돼 있는데 그 인스턴스를 그대로 다른 WPF Window의 Child로 동시에 종속시키는 것이 가능하지 않는 식입니다.

즉, 위의 명령어 실행은 Android의 axml layout UI 화면을 (이미 다른 Activity가 소유한) XAML 화면을 가져와 현재 Activity의 UI로 바꾸려는 시도가 됩니다.

그럼 어떻게 MainPage XAML로 갈 수 있을까요? 간단합니다. Activity 전환을 하면 됩니다. 따라서, Android의 방식에 따라 다음과 같이 Activity를 이동시켜야 합니다.

private void _btnSample_Click(object? sender, EventArgs e)
{
    // await Shell.Current.GoToAsync($"//{nameof(MainPage)}");

    var mainIntent = new Intent(this, typeof(MainActivity));
    StartActivity(mainIntent);
}




그런데, 여기서 또 한 가지 문제가 발생하는데요, 저 코드를 .NET 8/MAUI에서 실행하면 이런 오류 메시지를 보게 될 것입니다.

System.InvalidOperationException: 'Window was already created'

0xFFFFFFFFFFFFFFFF in Android.Runtime.RuntimeNativeMethods.monodroid_debugger_unhandled_exception   C#
0x1A in Android.Runtime.JNINativeWrapper._unhandled_exception at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:13,5 C#
0x1E in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:126,26  C#
0x12 in Microsoft.Maui.Controls.Window.Microsoft.Maui.IWindow.Created at D:\a\_work\1\s\src\Controls\src\Core\Window\Window.cs:483,5    C#
0xB in Microsoft.Maui.LifecycleEvents.AppHostBuilderExtensions.<>c.<OnConfigureLifeCycle>b__2_0 at D:\a\_work\1\s\src\Core\src\Hosting\LifecycleEvents\AppHostBuilderExtensions.Android.cs:23,6 C#
0xD in Microsoft.Maui.MauiAppCompatActivity. at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiAppCompatActivity.Lifecycle.cs:61,104  C#
0x1F in Microsoft.Maui.LifecycleEvents.LifecycleEventServiceExtensions.InvokeLifecycleEvents<Microsoft.Maui.LifecycleEvents.AndroidLifecycle.OnPostCreate> at D:\a\_work\1\s\src\Core\src\LifecycleEvents\LifecycleEventServiceExtensions.cs:31,5 C#
0x40 in Microsoft.Maui.MauiAppCompatActivity.OnPostCreate at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiAppCompatActivity.Lifecycle.cs:61,4   C#
0x11 in Android.App.Activity.n_OnPostCreate_Landroid_os_Bundle_ at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Android.App.Activity.cs:4488,4 C#
0x9 in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:125,5    C#

이슈를 보면 .NET 7 이하로는 괜찮았던 것 같은데 .NET 8부터 문제가 된다고 합니다.

MAUI Android build crashes when app is reopened from background. It throws the exception: 'Window was already created.' #18692
; https://github.com/dotnet/maui/issues/18692

위의 문제를 우회하는 방법이 하나 있는데요, 결국 OnPostCreate에서 예외가 발생한 것이기 때문에 아래와 같이 try/catch를 감싸주면 넘어가긴 합니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    )]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    }

    protected override void OnPostCreate(Bundle? savedInstanceState)
    {
        try
        {
            base.OnPostCreate(savedInstanceState);
        }
        catch (Exception e) // 하지만, 내부 코드의 어느 지점에서 예외가 발생했는지에 대해서는 알 수 없으므로 어떤 초기화가 생략됐을지 분석이 필요합니다.
        {
            System.Diagnostics.Trace.WriteLine(e.ToString());
        }
    }
}

위와 같은 현상이 발생하는 원인을 추측해 볼까요? 일단 예제 코드처럼 Intent 전환을 하면 Android는 새롭게 MainActivity 인스턴스를 생성해 처리합니다. 다시 말해 아래와 같은 Navigation Stack이 쌓여 있는 것입니다.

2: MainActivity // StartActivity(mainIntent)로 새로 쌓인 Activity
1: NewActivity  // currentActivity.StartActivity(newIntent)로 쌓인 Activity
0: MainActivity // 최초 App 실행 시 생성된 Activity

그렇다면 2개의 MainActivity 인스턴스가 생성됐고 XAML UI도 각각 새롭게 MainActivity에 붙어야 할 텐데요, 오류 메시지(Window was already created.)에서 알 수 있듯이 이미 한번 생성한 XAML UI를 중복 생성하는 바람에 예외 처리가 된 것입니다.

그래도 저 코드가 .NET 7 이하에서는 잘 동작했다고 하는 것을 보면 그전에는 (동작은 최초 생성한 MainActivity의 상태는 아니었겠지만) 중복 생성을 허용했었던 것을 .NET 8부터는 막은 것이 아닌가 생각됩니다. 물론 저만의 추측인데요, 그렇긴 한데 마이크로소프트는 저 이슈를 별다른 패치 없이 (아래에서 설명할 LaunchMode를 이용하라면서) 그냥 닫아버렸습니다.

그런 면에서 본다면, 엄밀히 MainActivity는 다중 인스턴스로 생성될 의미가 없습니다. 즉, 언제나/항상/무조건 LaunchMode 옵션을 SingleTask로 설정하는 것이 맞습니다.

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density
    , LaunchMode = LaunchMode.SingleTask // 참고: "그림으로 이해하는 Activity LaunchMode 실험"
    )]
public class MainActivity : MauiAppCompatActivity
{
    // ...[생략]...
}

저렇게 바꾸고 나면, 이제야 (OnPostCreate에서의 try/catch를 제거해도) 이 글의 예제에서 작성한 코드들이 아무런 문제 없이 동작하게 됩니다.




참고로, (SingleTask를 사용하지 않고) OnPostCreate에서의 try/catch를 사용하는 우회 방법이 통하지 않는 경우가 있습니다. 바로 MediaElement를 추가했을 때인데요,

<!-- 참고: C# - MAUI에서 MediaElement 사용 -->

<toolkit:MediaElement x:Name="mediaPlayer" />

재미있는 건 저렇게만 사용했을 때는 문제없지만 Event Handler를 걸게 되면,

<toolkit:MediaElement x:Name="mediaPlayer" Loaded="OnMediaLoaded" />

이제부터는 다른 Activity에서 MainActivity로 돌아올 때 이런 예외가 발생합니다.

System.ObjectDisposedException: 'Cannot access a disposed object.
Object name: 'CommunityToolkit.Maui.Core.Views.MauiMediaElement'.'

0xFFFFFFFFFFFFFFFF in Android.Runtime.RuntimeNativeMethods.monodroid_debugger_unhandled_exception   C#
0x1A in Android.Runtime.JNINativeWrapper._unhandled_exception at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:13,5 C#
0x23 in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLLL_L at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:368,26    C#
0x2E in Java.Interop.JniPeerMembers.AssertSelf at /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.cs:153,5    C#
0x1 in Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod at /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:57,5    C#
...[생략]...
0x5A in Microsoft.Maui.Platform.MauiContextExtensions.ToPlatform at D:\a\_work\1\s\src\Core\src\Platform\Android\MauiContextExtensions.cs:96,4  C#
0x2A in Microsoft.Maui.Controls.Platform.Compatibility.ShellFragmentContainer.OnCreateView at D:\a\_work\1\s\src\Controls\src\Core\Compatibility\Handlers\Shell\Android\ShellFragmentContainer.cs:26,4  C#
0x24 in AndroidX.Fragment.App.Fragment.n_OnCreateView_Landroid_view_LayoutInflater_Landroid_view_ViewGroup_Landroid_os_Bundle_ at C:\a\_work\1\s\generated\androidx.fragment.fragment\obj\Release\net6.0-android\generated\src\AndroidX.Fragment.App.Fragment.cs:2031,4 C#
0xD in Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLLL_L at /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:367,5  C#

이번에는 OnPostCreate 단계에서 발생하지 않기 때문에 응용 프로그램은 예외로 인해 종료하게 됩니다. 아마도 이런 문제 때문인지는 알 수 없으나, MediaElement는 애당초 문서에서 LaunchMode를 SingleTask로 지정하라고 명시하고 있습니다.

Platform specific initialization
; https://learn.microsoft.com/en-us/dotnet/communitytoolkit/maui/views/mediaelement?tabs=android#platform-specific-initialization

To initialize the MediaElement on Android, the LaunchMode of the applications Activity must be set to LaunchMode.SingleTask and you must add ResizableActivity=true as per the following example


언젠가는 마이크로소프트가 다중 XAML 초기화를 Activity마다 지원하게 될 수도 있을 것입니다. 적어도 그때까지는, 이런저런 이유로 인해 MainActivity의 LaunchMode는 항상 SingleTask로 지정하는 것이 여러모로 정신 건강에 이로울 듯합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/6/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2024-11-21 09시19분
Android쪽에서 activity 접근 할때 아래꺼 적어 놓고 참고 하고 있습니다. (Maui)
            //var context = Platform.AppContext;
            //Current Activity or null if not initialized or not started.
            //var activity = Platform.CurrentActivity;
            //var activity = await Platform.WaitForActivityAsync();
지현명

... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12563정성태3/16/202117192개발 환경 구성: 551. C# - JIRA REST API 사용 정리 (3) jira-oauth-cli 도구를 이용한 키 관리
12562정성태3/15/202117957개발 환경 구성: 550. C# - JIRA REST API 사용 정리 (2) JIRA OAuth 토큰으로 API 사용하는 방법파일 다운로드1
12561정성태3/12/202116714VS.NET IDE: 159. Visual Studio에서 개행(\n, \r) 등의 제어 문자를 치환하는 방법 - 정규 표현식 사용
12560정성태3/11/202117742개발 환경 구성: 549. ssh-keygen으로 생성한 PKCS#1 개인키/공개키 파일을 각각 PKCS8/PEM 형식으로 변환하는 방법
12559정성태3/11/202118003.NET Framework: 1028. 닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용 [2]파일 다운로드1
12558정성태3/10/202117106Windows: 192. Power Automate Desktop (Preview) 소개 - Bitvise SSH Client 제어 [1]
12557정성태3/10/202115323Windows: 191. 탐색기의 보안 탭에 있는 "Object name" 경로에 LEFT-TO-RIGHT EMBEDDING 제어 문자가 포함되는 문제
12556정성태3/9/202113578오류 유형: 703. PowerShell ISE의 Debug / Toggle Breakpoint 메뉴가 비활성 상태인 경우
12555정성태3/8/202116883Windows: 190. C# - 레지스트리에 등록된 DigitalProductId로부터 라이선스 키(Product Key)를 알아내는 방법파일 다운로드2
12554정성태3/8/202116444.NET Framework: 1027. 닷넷 응용 프로그램을 위한 PDB 옵션 - full, pdbonly, portable, embedded
12553정성태3/5/202116437개발 환경 구성: 548. 기존 .NET Framework 프로젝트를 .NET Core/5+ 용으로 변환해 주는 upgrade-assistant, try-convert 도구 소개 [4]
12552정성태3/5/202115882개발 환경 구성: 547. github workflow/actions에서 Visual Studio Marketplace 패키지 등록하는 방법
12551정성태3/5/202114261오류 유형: 702. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly. (2)
12550정성태3/5/202113987오류 유형: 701. Live Share 1.0.3713.0 버전을 1.0.3884.0으로 업데이트 이후 ContactServiceModelPackage 오류 발생하는 문제
12549정성태3/4/202115290오류 유형: 700. VsixPublisher를 이용한 등록 시 다양한 오류 유형 해결책
12548정성태3/4/202116399개발 환경 구성: 546. github workflow/actions에서 nuget 패키지 등록하는 방법
12547정성태3/3/202117062오류 유형: 699. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly.
12546정성태3/3/202116902개발 환경 구성: 545. github workflow/actions에서 빌드시 snk 파일 다루는 방법 - Encrypted secrets
12545정성태3/2/202119758.NET Framework: 1026. 닷넷 5에 추가된 POH (Pinned Object Heap) [10]
12544정성태2/26/202119961.NET Framework: 1025. C# - Control의 Invalidate, Update, Refresh 차이점 [2]
12543정성태2/26/202117957VS.NET IDE: 158. C# - 디자인 타임(design-time)과 런타임(runtime)의 코드 실행 구분
12542정성태2/20/202119630개발 환경 구성: 544. github repo의 Release 활성화 및 Actions를 이용한 자동화 방법 [1]
12541정성태2/18/202117188개발 환경 구성: 543. 애저듣보잡 - Github Workflow/Actions 소개
12540정성태2/17/202118289.NET Framework: 1024. C# - Win32 API에 대한 P/Invoke를 대신하는 Microsoft.Windows.CsWin32 패키지
12539정성태2/16/202118209Windows: 189. WM_TIMER의 동작 방식 개요파일 다운로드1
12538정성태2/15/202118683.NET Framework: 1023. C# - GC 힙이 아닌 Native 힙에 인스턴스 생성 - 0SuperComicLib.LowLevel 라이브러리 소개 [2]
... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...